Skip to content

feat: human-in-the-loop support with AskUserQuestion#871

Merged
Gkrumbach07 merged 18 commits intomainfrom
feat/human-in-the-loop
Mar 12, 2026
Merged

feat: human-in-the-loop support with AskUserQuestion#871
Gkrumbach07 merged 18 commits intomainfrom
feat/human-in-the-loop

Conversation

@Gkrumbach07
Copy link
Contributor

Summary

  • AskUserQuestion UI: Interactive question component with single/multi-select options, freeform "Other" input, and multi-question tabbed navigation
  • Agent status from event log: Derive agentStatus at query time from persisted AG-UI events instead of caching on the CR (eliminates goroutine race conditions)
  • Frontend status fix: useAgentStatus hook reads raw PlatformMessage.toolCalls[] format, correctly detecting unanswered AskUserQuestion tool calls
  • TOOL_CALL_RESULT emission: Adapter emits TOOL_CALL_RESULT on next run for halted tool calls so the frontend transitions questions to answered state
  • Session status indicators: SessionStatusDot and AgentStatusIndicator components for detail and table views
  • CRD cleanup: Removed agentStatus field from CRD schema — no longer stored, only derived

Supersedes #725.

Test plan

  • Create session, send prompt that triggers AskUserQuestion
  • Verify question UI appears with options
  • Verify session table shows waiting_input status
  • Verify detail page shows waiting_input status
  • Submit answer, verify question transitions to answered (green) state
  • Verify agent resumes after answer

🤖 Generated with Claude Code

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds agent-level status tracking derived from AGUI event logs, wires it through backend handlers and websocket storage, exposes it via session APIs, and surfaces interactive human-in-the-loop AskUserQuestion flows and UI indicators in the frontend; includes runner-side halt handling and tests for status derivation.

Changes

Cohort / File(s) Summary
Configuration
/.gitignore, /Makefile
Ignore pattern for Claude worktrees added; trap on INT updated to kill process group and port-forward commands launched as separate background jobs.
Backend Types & Handlers
components/backend/types/agui.go, components/backend/types/session.go, components/backend/handlers/sessions.go, components/backend/main.go
New agent-status constants and optional AgentStatus field; added DeriveAgentStatusFromEvents hook, enrichAgentStatus usage in List/Get sessions, CreateSession timestamp use of UnixMilli, and wiring of websocket-derived status in main.
Backend Websocket Store & Proxy
components/backend/websocket/agui_store.go, components/backend/websocket/agui_proxy.go
Added DeriveAgentStatus(sessionID) to tail-read agui-events.jsonl and map lifecycle/tool events to status; switched persistence keys to namespacedSessionID; added isAskUserQuestionToolCall helper and updated proxy stream/publish signatures to use namespaced IDs.
Backend Tests
components/backend/websocket/agui_store_test.go
Comprehensive tests for DeriveAgentStatus covering lifecycle events, AskUserQuestion detection, run scoping, casing, and missing files.
Frontend Types
components/frontend/src/types/agentic-session.ts, components/frontend/src/types/api/sessions.ts
Added AgentStatus/StoredAgentStatus unions, AskUserQuestion input types, and optional persisted agentStatus on session status.
Frontend Hooks & State
components/frontend/src/hooks/use-agent-status.ts, components/frontend/src/hooks/use-agui-stream.ts, components/frontend/src/hooks/agui/types.ts, components/frontend/src/hooks/use-sessions.ts
New useAgentStatus and deriveAgentStatusFromPhase; extended sendMessage to accept optional metadata; added isRunActive and several pending maps to AGUI state; added smart polling for sessions.
Frontend UI Components
components/frontend/src/components/agent-status-indicator.tsx, components/frontend/src/components/session-status-dot.tsx
New visual components: AgentStatusIndicator and SessionStatusDot for phase and agent-level status display with animations/tooltips.
Frontend Messaging & Interaction
components/frontend/src/components/session/ask-user-question.tsx, components/frontend/src/components/session/MessagesTab.tsx, components/frontend/src/components/ui/stream-message.tsx, components/frontend/src/components/ui/tool-message.tsx
New AskUserQuestionMessage component for single/multi-select questions with "Other" input; MessagesTab and StreamMessage accept onSendToolAnswer/onSubmitAnswer to submit hidden ask_user_question_response messages; tool-message summarizes AskUserQuestion tool usage.
Frontend Page & Sessions Table
components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx, components/frontend/src/components/workspace-sections/sessions-section.tsx
Wired useAgentStatus into session page header/breadcrumbs, added sendToolAnswer handler, replaced inline badges with AgentStatusIndicator and SessionStatusDot in sessions listing.
Runners / Adapter / Prompts
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py, components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py, components/runners/ambient-runner/ambient_runner/platform/prompts.py
Introduced BUILTIN_FRONTEND_TOOLS including AskUserQuestion, added per-run halt tracking (_halted, _halted_tool_call_id, halted property), emit TOOL_CALL_RESULT when resuming halted calls, log/interrupt worker when adapter halted after streams, and appended HUMAN_INPUT_INSTRUCTIONS into prompts.

Sequence Diagrams

sequenceDiagram
    participant Client as Frontend Client
    participant API as Backend API
    participant Handler as Session Handler
    participant EventStore as AGUI Event Store
    participant WS as Websocket Store

    Client->>API: GET /sessions/{id}
    API->>Handler: GetSession
    Handler->>Handler: enrichAgentStatus(session)
    Handler->>WS: DeriveAgentStatusFromEvents(namespacedSessionID)
    WS->>EventStore: read agui-events.jsonl (tail)
    EventStore->>WS: parse lifecycle/tool events
    WS-->>Handler: AgentStatus ("working"|"idle"|"waiting_input"|"")
    Handler->>API: return session with session.status.agentStatus
    API-->>Client: respond JSON
    Client->>Client: useAgentStatus(phase,isRunActive,messages)
    Client->>Client: render AgentStatusIndicator / AskUserQuestion UI
Loading
sequenceDiagram
    participant User as User/UI
    participant Frontend as Frontend Page
    participant Hook as useAGUIStream Hook
    participant API as Backend API
    participant Runner as Agent Runner
    participant Adapter as ClaudeAgentAdapter

    User->>Frontend: Answer AskUserQuestionMessage
    Frontend->>Hook: sendToolAnswer(formattedAnswer, metadata)
    Hook->>API: POST run request with ask_user_question_response
    API->>Runner: deliver message
    Runner->>Adapter: resume halted tool call
    Adapter->>Runner: emit TOOL_CALL_RESULT for halted_tool_call_id
    Runner->>API: emit subsequent events
    API-->>Frontend: stream events
    Frontend->>Frontend: update messages and agentStatus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature being added: human-in-the-loop support via AskUserQuestion tool/UI, which is the primary focus of the changeset across frontend, backend, and adapter components.
Description check ✅ Passed The description comprehensively covers the changeset, detailing the AskUserQuestion UI component, agent status derivation from events, frontend status handling fixes, adapter changes, status indicators, and CRD cleanup—all of which align with the actual file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/human-in-the-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

Summary

This PR adds human-in-the-loop (HITL) support via the AskUserQuestion tool, replacing a stale CR-cached agentStatus field with real-time derivation from the persisted AG-UI event log. The approach is architecturally sound — eliminating goroutine race conditions by making the event log the single source of truth — and the frontend UX work (tabbed multi-question UI, smart polling tiers, session status indicators) is well-executed. A few issues need attention before merge.

Issues by Severity

Blocker Issues

None.

Critical Issues

None.

Major Issues

1. Unbounded file read on every ListSessions / GetSession call

components/backend/websocket/agui_store.goDeriveAgentStatus reads the full agui-events.jsonl from disk on every API call for each Running session via os.ReadFile(path). Event logs are append-only and grow unboundedly. With long-running sessions or many concurrent Running sessions in a project, this could add significant latency. There is no size cap, line-count limit, or caching layer.

Suggested mitigation: Read only the last N KB (os.Open + Seek to tail), since DeriveAgentStatus scans backwards and only needs the most recent lifecycle events. Alternatively, a short-lived TTL cache keyed on sessionID prevents redundant disk reads within a polling interval.


2. agentStatus field typed inconsistently with the exported AgentStatus type

components/frontend/src/types/agentic-session.ts:187 and src/types/api/sessions.ts:94 both use an inline 3-value literal — "working" | "idle" | "waiting_input" — while the exported AgentStatus type has 5 values (adds "completed" and "failed"). AgentStatusIndicator accepts all 5, but code reading session.status?.agentStatus gets only the 3-value type, creating a silent narrowing mismatch that will confuse future callers.

Fix: Either reference AgentStatus directly, or export a StoredAgentStatus = "working" | "idle" | "waiting_input" alias and use it in both type files.


3. Missing test coverage for DeriveAgentStatus backward-scan logic

components/backend/websocket/agui_store.go:197-261 — The backward-scanning event parser has non-trivial branches, especially the cross-run-ID matching in the EventTypeToolCallStart case where a TOOL_CALL_START from a different runId short-circuits to "idle". None of these branches are covered by tests in this PR. Complex stateful parsing logic without tests is a regression risk.

Minimum test cases: (a) empty file, (b) RUN_STARTED only → "working", (c) RUN_FINISHED + same-run AskUserQuestion TOOL_CALL_START → "waiting_input", (d) RUN_FINISHED + different-run TOOL_CALL_START → "idle".


4. Unused import in sessions-section.tsx

components/frontend/src/components/workspace-sections/sessions-section.tsx:25SessionPhaseBadge is imported but its only usage was replaced with AgentStatusIndicator. Dead code; should fail ESLint.

Minor Issues

1. Extra blank lines in MessagesTab.tsx:87-88 — Two consecutive blank lines added between useEffect blocks, inconsistent with surrounding style.

2. AskUserQuestionMessage in components/ui/components/ui/ is the Shadcn primitives directory. This stateful, business-logic HITL component belongs in components/session/ or a colocated _components/ directory.

3. Double type cast in ask-user-question.tsx:29input as unknown as AskUserQuestionInput should use a type guard for safety and clarity, consistent with the project preference for avoiding unsafe casts.

Positive Highlights

  • Elegant architecture: Deriving agentStatus at query time from the event log eliminates the goroutine race condition and makes the event log the authoritative source of truth — the right long-term design.
  • Smart polling tiers (use-sessions.ts): 2s transitioning → 5s working → 15s idle → off reduces unnecessary backend load without sacrificing responsiveness.
  • AskUserQuestionMessage UX: Auto-advance on single-select, tabbed multi-question navigation, "Other" freeform input, amber → green visual distinction — all thoughtful details.
  • TOOL_CALL_RESULT emission on next run: Clean solution for transitioning questions to answered state without an extra round-trip.
  • Millisecond session name timestamp: Straightforward fix for burst-creation name collisions.
  • isAskUserQuestionToolCall normalization: Case-insensitive alpha-only comparison mirrors the frontend pattern — good cross-layer parity.
  • Nil-guard on DeriveAgentStatusFromEvents: Correctly nil-checked in enrichAgentStatus before use.
  • Makefile port-forward fix: kill 0 correctly kills the entire process group — a real bug fix.

Recommendations

  1. [Major — performance] Limit the event log read to a tail window or add a short-lived cache before shipping to users with active long-running sessions.
  2. [Major — tests] Add unit tests for DeriveAgentStatus covering the cross-run-ID edge cases.
  3. [Major — types] Replace the inline 3-value literal with a shared type alias in both frontend type files.
  4. [Major — lint] Remove the unused SessionPhaseBadge import from sessions-section.tsx.
  5. [Minor] Move ask-user-question.tsx out of components/ui/ into a session-specific directory.

Generated with Claude Code

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
components/operator/internal/handlers/reconciler.go (1)

83-87: ⚠️ Potential issue | 🟠 Major

Remove the reconciler-side agentStatus patches too.

The controller-runtime path is still persisting status.agentStatus even though the new design derives it from AG-UI event logs. Leaving these writes here keeps the old CR-backed status path alive in parallel with the new derived path, so the system can still disagree about whether a session is idle vs. waiting for input.

Also applies to: 130-133, 241-244, 259-261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/operator/internal/handlers/reconciler.go` around lines 83 - 87,
The reconciler is still writing status.agentStatus; remove any calls that set
agentStatus from the controller side so the field is no longer patched by the
reconciler (leave other status fields intact). Specifically, in reconciler.go
remove the SetField("agentStatus", ...) calls created via NewStatusPatch (the
occurrences near the shown block and the other occurrences referenced around
lines 130-133, 241-244, and 259-261) so the controller no longer persists
agentStatus; keep the rest of the status.SetField calls (phase, completionTime,
stoppedReason, etc.) unchanged.
components/frontend/src/hooks/agui/types.ts (1)

35-50: ⚠️ Potential issue | 🟠 Major

Replace singleton initialState with a factory function to ensure fresh Map instances per hook mount.

The code correctly uses immutable updates—all Map mutations follow the pattern new Map(state.pendingToolCalls) → mutate → reassign—preventing in-place mutations. However, useState(initialState) shares the same Map() instances across concurrent hook mounts. While event handlers avoid state leakage through immutable patterns, the singleton design is fragile and violates React state best practices. Use a factory function to guarantee each hook receives fresh containers:

export const createInitialState = (): AGUIClientState => ({
  threadId: null,
  runId: null,
  status: 'idle',
  messages: [],
  state: {},
  activities: [],
  currentMessage: null,
  currentToolCall: null,
  pendingToolCalls: new Map(),
  pendingChildren: new Map(),
  error: null,
  messageFeedback: new Map(),
  currentReasoning: null,
  currentThinking: null,
})

Then update the hook: const [state, setState] = useState(createInitialState())

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/hooks/agui/types.ts` around lines 35 - 50, The
singleton initialState exports shared Map instances which can be reused across
hook mounts; replace it with a factory that returns a fresh AGUIClientState
object (e.g., createInitialState(): AGUIClientState) that constructs new Map()s
each call, then update the hook usage to call useState(createInitialState())
instead of useState(initialState), keeping all property names (threadId,
pendingToolCalls, pendingChildren, messageFeedback, etc.) identical.
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py (1)

888-923: ⚠️ Potential issue | 🔴 Critical

Clear the tool-call state before halting the stream.

This branch already emits TOOL_CALL_END, but it leaves current_tool_call_id populated. The next loop iteration breaks, then the finally block at Lines 1085-1096 emits a second TOOL_CALL_END for the same tool call. That double-closes the HITL tool and can desync the frontend.

Suggested fix
                             if is_frontend_tool:
                                 # Flush before halt (message_stop won't fire after interrupt)
                                 flush_pending_msg()

                                 # Emit TOOL_CALL_END for frontend tool (client needs this to know call is complete)
                                 yield ToolCallEndEvent(
                                     type=EventType.TOOL_CALL_END,
                                     thread_id=thread_id,
                                     run_id=run_id,
                                     tool_call_id=current_tool_call_id,
                                     timestamp=now_ms(),
                                 )

                                 if current_message_id and has_streamed_text:
                                     yield TextMessageEndEvent(
                                         type=EventType.TEXT_MESSAGE_END,
                                         thread_id=thread_id,
                                         run_id=run_id,
                                         message_id=current_message_id,
                                         timestamp=now_ms(),
                                     )
                                     current_message_id = None

                                 logger.debug(
                                     f"Frontend tool halt: {current_tool_display_name}"
                                 )

                                 # NOTE: interrupt is the caller's responsibility
                                 # (e.g. worker.interrupt() from the platform layer).
                                 # Check adapter.halted after the stream ends.

                                 self._halted = True
                                 self._halted_tool_call_id = current_tool_call_id
+                                current_tool_call_id = None
+                                current_tool_call_name = None
+                                current_tool_display_name = None
+                                accumulated_tool_json = ""
                                 halt_event_stream = True
                                 # Continue consuming remaining events for cleanup
                                 continue

Based on learnings, Tool calls in AG-UI event stream MUST be wrapped in TOOL_CALL_START → TOOL_CALL_ARGS (1+) → TOOL_CALL_END sequence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py` around lines
888 - 923, The frontend-halt branch emits a ToolCallEndEvent but leaves
current_tool_call_id populated, causing the finally block to emit a duplicate
TOOL_CALL_END; fix by clearing the tool-call state immediately after emitting
the end events and before setting halted flags — specifically set
current_tool_call_id = None (and also clear current_tool_display_name if
present) right after yielding ToolCallEndEvent (and after clearing
current_message_id when applicable), so the later cleanup/finally logic won't
re-emit the same TOOL_CALL_END.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/backend/handlers/sessions.go`:
- Around line 673-676: The current CreateSession code builds a deterministic CR
name using the millisecond timestamp (variables timestamp and name), which can
collide and causes an AlreadyExists error that surfaces as a 500; change the
session CR creation to use Kubernetes server-side name generation (set
ObjectMeta.GenerateName like "session-<ns>-") or implement a small retry loop
that appends a random suffix to name on apierrors.IsAlreadyAlreadyExists
collisions (and retry a few times) instead of failing; ensure the handler
(CreateSession) explicitly detects apierrors.IsAlreadyExists and either retries
or returns a 409/meaningful client error rather than an internal 500.
- Around line 439-440: The handler currently calls enrichAgentStatus(&session)
while iterating the full sessions result set, causing event-log lookups for
every running session even though only paginatedSessions are returned; move the
agentStatus derivation to after pagination by invoking enrichAgentStatus for
each session in the paginatedSessions slice (not the original sessions slice) so
event-log reads scale with page size rather than the full query.
- Around line 44-46: The event store currently uses only sessionName which
allows cross-project collisions; update the storage key to include namespace
(e.g., {StateBaseDir}/sessions/{namespace}/{sessionName}/agui-events.jsonl) and
change all related functions to accept and propagate namespace: modify
persistEvent(...) and loadEvents(...) to build and use the namespace-qualified
path, update the signature and callers of DeriveAgentStatusFromEvents to accept
(namespace, sessionName) and adjust
DeriveAgentStatus/DeriveAgentStatusFromEvents implementation accordingly, and
ensure enrichAgentStatus() calls DeriveAgentStatusFromEvents with both namespace
and sessionName; alternatively you may switch to using the session UID as the
unique store key if preferred, but be consistent across persist/load/derive
callers.

In `@components/backend/websocket/agui_store.go`:
- Around line 197-256: DeriveAgentStatus currently reads the entire
agui-events.jsonl (via os.ReadFile and splitLines) and reparses every line;
instead, make it use a cheap, constant-time source of truth: read a per-session
compact status record (e.g. sessions/<sessionID>/agui-status.json or a small
"last-status" file) that is updated whenever new events are appended, and only
fall back to scanning a small tail or the full log if that status file is
missing/corrupt. Update the event-appending flow (where events are written to
StateBaseDir/sessions/<sessionID>/agui-events.jsonl) to also update this compact
status file with the latest derived status, and change
DeriveAgentStatus(sessionID) to prefer reading that file (and only scan the log
as a fallback), keeping the existing runEndRunID/logic for fallback correctness.

In `@components/frontend/src/components/session-status-dot.tsx`:
- Around line 39-48: The span used as the TooltipTrigger child in
SessionStatusDot places an aria-label on a non-focusable element (the span)
causing Biome lint errors; change the trigger element to a focusable element
(e.g., a button) or make the dot purely decorative by removing the aria-label
and using aria-hidden. Specifically update the TooltipTrigger child currently
rendering <span ... aria-label={`Session: ${phase}`} />: either replace that
span with a focusable element (button) and preserve the aria-label for
keyboard/screen-reader access, or remove the aria-label and add
aria-hidden="true" (and adjust styling to keep the small dot) so the
TooltipTrigger no longer exposes an invalid aria attribute; modify usage around
the className, color, animation, and phase variables in SessionStatusDot
accordingly.

In `@components/frontend/src/components/session/MessagesTab.tsx`:
- Around line 143-151: The current StreamMessage key uses the unstable index
(`sm-${idx}`) causing remounts when filteredMessages changes; update the key on
the mapped StreamMessage to use a stable identifier: use the tool use block id
for ToolUseMessages (e.g., m.toolUseBlock.id) and m.id for all other message
types (or fallback to whichever ID is present) so components like StreamMessage
and its children (including AskUserQuestionMessage) preserve their local state
when showSystemMessages toggles.

In `@components/frontend/src/components/ui/ask-user-question.tsx`:
- Around line 138-161: The "Other" custom row must be converted to use a real
form control and keyboard-accessible handlers: replace the custom div/button
with a labeled Checkbox/Radio input (matching the existing Checkbox usage) or
native input element tied to the same state handlers so keyboard focus and label
association work; ensure the input uses the same checked/disabled props and
calls handleMultiSelect(q.question, "Other", checked === true) (or an onChange
that forwards the checked state) instead of onClick, give the input an id and
use htmlFor on the label to associate them, and if text entry is required keep a
separate text input but do not rely on clicking the surrounding div to toggle
selection—use onCheckedChange/onChange for keyboard accessibility and to
integrate with the form control system already used in this component.
- Around line 275-285: The button in the tab list (the element using onClick={()
=> setActiveTab(idx)} and className computed with cn and the active/done flags)
lacks an explicit type and will default to "submit" inside forms; update that
<button> to include type="button" attribute so it does not trigger form
submission when clicked (i.e., add type="button" to the button that calls
setActiveTab).
- Around line 108-121: handleSubmit currently calls onSubmitAnswer and
immediately sets submitted, which can lock the UI if the send fails; make
onSubmitAnswer return a Promise<void>, add an isSubmitting state flag, convert
handleSubmit to async, set isSubmitting=true before calling await
onSubmitAnswer(response), only call setSubmitted(true) after the await
completes, and in the catch block clear isSubmitting and surface the error (do
not set submitted); also update the page-level sendToolAnswer to re-throw errors
instead of swallowing them so the component can detect failures.

In `@components/frontend/src/services/queries/use-sessions.ts`:
- Around line 56-64: The 5s polling tier currently only matches sessions where
status.agentStatus === 'working', but backend can return an empty string or omit
agentStatus for active Running sessions; update the predicate in use-sessions.ts
(the hasWorking check) to treat a missing/empty agentStatus as active: when
s.status?.phase === 'Running' && (s.status?.agentStatus === 'working' ||
!s.status?.agentStatus) then return 5000. Keep the hasRunning check as-is for
other Running sessions.

In `@components/operator/internal/handlers/sessions.go`:
- Around line 1908-1913: The operator is writing status.agentStatus in several
terminal paths (e.g., in sessions.go where pod.Status.Phase ==
corev1.PodSucceeded and similar blocks around the other noted ranges), which
must be removed so agentStatus is derived at query-time; remove any
statusPatch.SetField("agentStatus", ...) calls (and related writes) from those
branches (including the blocks at the given ranges) and only leave
phase/timestamps/reason/conditions updates, ensuring statusPatch.Apply() still
runs where appropriate.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 124-132: The check uses the shared adapter state
(self._adapter.halted / ClaudeAgentAdapter._halted_tool_call_id) which is
mutable across runs and can race between concurrent thread_id runs; change the
logic in ClaudeBridge to capture the halt outcome for the specific run
invocation (e.g., have run() return or produce a per-run halted flag or
halted_tool_call_id) and use that per-run value when deciding to call await
worker.interrupt(), or alternatively switch adapter state to be keyed by
thread_id/worker and read that per-thread value instead of self._adapter.halted
so only the current run's halt triggers interrupting the worker.

In `@Makefile`:
- Around line 738-739: The port-forward commands hard-code the namespace
"ambient-code" causing make target kind-port-forward to ignore NAMESPACE; update
the two kubectl port-forward invocations (the lines invoking
svc/frontend-service and svc/backend-service that use KIND_FWD_FRONTEND_PORT and
KIND_FWD_BACKEND_PORT) to use the variable $(NAMESPACE) instead of the literal
ambient-code so the target respects the configurable namespace.
- Around line 737-739: The trap currently uses kill 0 and hard-codes the
namespace; change it to capture each background kubectl PID (after the kubectl
port-forward lines use $! to record PIDs into variables) and in the INT trap
explicitly kill those PIDs (not kill 0), then print the success message; also
replace the hard-coded "ambient-code" namespace with the Makefile variable
$(NAMESPACE) for both frontend and backend port-forward commands, and ensure the
recorded PID variables (for the frontend/backend port-forwards) are referenced
in the trap for clean shutdown.

---

Outside diff comments:
In `@components/frontend/src/hooks/agui/types.ts`:
- Around line 35-50: The singleton initialState exports shared Map instances
which can be reused across hook mounts; replace it with a factory that returns a
fresh AGUIClientState object (e.g., createInitialState(): AGUIClientState) that
constructs new Map()s each call, then update the hook usage to call
useState(createInitialState()) instead of useState(initialState), keeping all
property names (threadId, pendingToolCalls, pendingChildren, messageFeedback,
etc.) identical.

In `@components/operator/internal/handlers/reconciler.go`:
- Around line 83-87: The reconciler is still writing status.agentStatus; remove
any calls that set agentStatus from the controller side so the field is no
longer patched by the reconciler (leave other status fields intact).
Specifically, in reconciler.go remove the SetField("agentStatus", ...) calls
created via NewStatusPatch (the occurrences near the shown block and the other
occurrences referenced around lines 130-133, 241-244, and 259-261) so the
controller no longer persists agentStatus; keep the rest of the status.SetField
calls (phase, completionTime, stoppedReason, etc.) unchanged.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 888-923: The frontend-halt branch emits a ToolCallEndEvent but
leaves current_tool_call_id populated, causing the finally block to emit a
duplicate TOOL_CALL_END; fix by clearing the tool-call state immediately after
emitting the end events and before setting halted flags — specifically set
current_tool_call_id = None (and also clear current_tool_display_name if
present) right after yielding ToolCallEndEvent (and after clearing
current_message_id when applicable), so the later cleanup/finally logic won't
re-emit the same TOOL_CALL_END.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b7d7c7d-4ea6-4ef7-8401-88a675cabb23

📥 Commits

Reviewing files that changed from the base of the PR and between 5d96404 and cb1c8c2.

⛔ Files ignored due to path filters (1)
  • components/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • .gitignore
  • Makefile
  • components/backend/handlers/sessions.go
  • components/backend/main.go
  • components/backend/types/agui.go
  • components/backend/types/session.go
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
  • components/frontend/src/components/agent-status-indicator.tsx
  • components/frontend/src/components/session-status-dot.tsx
  • components/frontend/src/components/session/MessagesTab.tsx
  • components/frontend/src/components/ui/ask-user-question.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/components/ui/tool-message.tsx
  • components/frontend/src/components/workspace-sections/sessions-section.tsx
  • components/frontend/src/hooks/agui/types.ts
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/hooks/use-agui-stream.ts
  • components/frontend/src/services/queries/use-sessions.ts
  • components/frontend/src/types/agentic-session.ts
  • components/frontend/src/types/api/sessions.ts
  • components/operator/internal/handlers/reconciler.go
  • components/operator/internal/handlers/sessions.go
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/ambient_runner/platform/prompts.py

Comment on lines +44 to +46
// DeriveAgentStatusFromEvents derives agentStatus from the persisted event log.
// Set by the websocket package at init to avoid circular imports.
DeriveAgentStatusFromEvents func(sessionID string) string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how agent-status derivation is wired and what key the persisted event store uses.
# Expected: the implementation should use a namespace-qualified or otherwise globally unique key.

rg -n -C3 '\bDeriveAgentStatusFromEvents\b|\bDeriveAgentStatus\b'
rg -n -C3 'sdkSession(Id|ID)|sessionName|namespace|project'

Repository: ambient-code/platform

Length of output: 50381


🏁 Script executed:

# Find where sessions are created and how sessionID/name are set
rg -n 'session.*ID|sessionID|metadata\["name"\]|Metadata.*name' components/backend/handlers/sessions.go -A 2 -B 2

# Check what StateBaseDir contains and how session directories are structured  
rg -n 'StateBaseDir|sessions/' components/backend/websocket/ -A 2 -B 2

# Look for session creation to understand ID generation
rg -n 'CreateSession|GenerateName|UnixMilli' components/backend/handlers/sessions.go -A 5 -B 2

Repository: ambient-code/platform

Length of output: 5144


🏁 Script executed:

# Find all calls to persistEvent to see what sessionID value is passed
rg -n 'persistEvent' components/backend/websocket/ -B 3 -A 1

# Search for where events are written to understand the sessionID context
rg -n 'HandleMessage|broadcastEvent|persistEvent' components/backend/websocket/agui_proxy.go -B 5 -A 5

Repository: ambient-code/platform

Length of output: 4352


🏁 Script executed:

# Find function definitions and signatures in agui_proxy.go to understand sessionName and sessionID
rg -n 'func.*sessionName|func.*sessionID|persistStreamedEvent|HandleMessage|HandleAGUI' components/backend/websocket/agui_proxy.go -A 15 | head -100

# Look for where sessionName is set/extracted
rg -n 'sessionName\s*:=|sessionName\s*=' components/backend/websocket/agui_proxy.go -B 3 -A 2

Repository: ambient-code/platform

Length of output: 6105


🏁 Script executed:

# Verify that sessionID in persistStreamedEvent is actually the bare sessionName
rg -n 'persistStreamedEvent' components/backend/websocket/agui_proxy.go -B 10 -A 5

# Check StateBaseDir initialization and scope
rg -n 'StateBaseDir\s*=' components/backend/ -B 2 -A 2

# Understand if StateBaseDir is per-session or shared
rg -n 'STATE_BASE_DIR|StateBaseDir' components/backend/main.go -B 3 -A 3

Repository: ambient-code/platform

Length of output: 3936


🏁 Script executed:

# Check if backend is deployed per-namespace or per-cluster
fd -t f 'backend.*deployment\|backend.*yaml' components/manifests/ --type f | head -20

# Look at deployment manifests to understand scope
rg -n 'namespace|spec:' components/manifests/ -l | grep -i backend | head -5

Repository: ambient-code/platform

Length of output: 371


🏁 Script executed:

# Find and read backend deployment files
find components/manifests -name "*backend*" -type f | grep -E 'deployment|kustomization' | head -10

# Check for kustomization files that define the backend
fd 'kustomization' components/manifests/base -o components/manifests/overlays

# Look for backend pod template specifications
rg -n 'kind: Deployment|kind: Pod|spec:' components/manifests/base/ -l | grep -i backend

Repository: ambient-code/platform

Length of output: 483


🏁 Script executed:

# Read backend deployment to check namespace and StateBaseDir setup
cat -n components/manifests/base/backend-deployment.yaml | head -80

# Check how StateBaseDir is used and if there's any namespace qualification
rg -n 'STATE_BASE_DIR|StateBaseDir|env:' components/manifests/base/backend-deployment.yaml -B 2 -A 2

Repository: ambient-code/platform

Length of output: 3269


🏁 Script executed:

# Check if backend is deployed per-namespace (in kustomization)
rg -n 'backend-deployment' components/manifests/base/kustomization.yaml -B 2 -A 2

# Check namespace in kustomization
rg -n 'namespace:' components/manifests/base/kustomization.yaml

# Verify if sessions can be created with same name in different namespaces
rg -n 'namespace|project' components/backend/handlers/sessions.go | grep -i create -A 10 -B 10 | head -50

Repository: ambient-code/platform

Length of output: 4377


🏁 Script executed:

# Check if backend serves multiple projects/namespaces or is single-tenant
rg -n 'project.*Param|projectName' components/backend/handlers/sessions.go | head -20

# Verify StateBaseDir is shared/mounted and if it's truly unqualified
rg -n 'StateBaseDir|/workspace' components/manifests/ -r

# Check if PVC is shared across namespaces or single-namespace
rg -n 'volumeMounts|persistentVolume' components/manifests/base/backend-deployment.yaml -B 2 -A 5

Repository: ambient-code/platform

Length of output: 2506


Cross-project session collision in agent status derivation — use namespace-qualified keys in event store.

Sessions can be created with identical names in different projects if created within the same millisecond (e.g., session-1709251234567). The event store path uses only the bare session name — {StateBaseDir}/sessions/{sessionName}/agui-events.jsonl — without namespace qualification. This allows two projects' sessions with the same name to collide and read/write the same event file, causing enrichAgentStatus() to derive incorrect agentStatus for affected sessions.

To fix:

  • Qualify the event store key with namespace: e.g., {StateBaseDir}/sessions/{namespace}/{sessionName}/agui-events.jsonl
  • Update persistEvent(), loadEvents(), DeriveAgentStatus(), and all callers to use the namespace-qualified path
  • Ensure enrichAgentStatus() passes both namespace and session name to DeriveAgentStatusFromEvents()

Alternatively, use the session's UID instead of the name as the store key, since UIDs are globally unique within Kubernetes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 44 - 46, The event
store currently uses only sessionName which allows cross-project collisions;
update the storage key to include namespace (e.g.,
{StateBaseDir}/sessions/{namespace}/{sessionName}/agui-events.jsonl) and change
all related functions to accept and propagate namespace: modify
persistEvent(...) and loadEvents(...) to build and use the namespace-qualified
path, update the signature and callers of DeriveAgentStatusFromEvents to accept
(namespace, sessionName) and adjust
DeriveAgentStatus/DeriveAgentStatusFromEvents implementation accordingly, and
ensure enrichAgentStatus() calls DeriveAgentStatusFromEvents with both namespace
and sessionName; alternatively you may switch to using the session UID as the
unique store key if preferred, but be consistent across persist/load/derive
callers.

Comment on lines +673 to 676
// Generate unique name (millisecond timestamp for burst-creation safety)
// Note: Runner will create branch as "ambient/{session-name}"
timestamp := time.Now().Unix()
timestamp := time.Now().UnixMilli()
name := fmt.Sprintf("session-%d", timestamp)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Millisecond timestamps still allow name collisions.

Two CreateSession requests in the same millisecond for the same namespace will produce the same CR name and one of them will fail with AlreadyExists, which this path currently turns into a 500. Please switch to Kubernetes generateName or retry with a random suffix on collision.

As per coding guidelines, "Handle errors and edge cases explicitly rather than ignoring them."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 673 - 676, The current
CreateSession code builds a deterministic CR name using the millisecond
timestamp (variables timestamp and name), which can collide and causes an
AlreadyExists error that surfaces as a 500; change the session CR creation to
use Kubernetes server-side name generation (set ObjectMeta.GenerateName like
"session-<ns>-") or implement a small retry loop that appends a random suffix to
name on apierrors.IsAlreadyAlreadyExists collisions (and retry a few times)
instead of failing; ensure the handler (CreateSession) explicitly detects
apierrors.IsAlreadyExists and either retries or returns a 409/meaningful client
error rather than an internal 500.

Comment on lines 1908 to 1913
if pod.Status.Phase == corev1.PodSucceeded {
statusPatch.SetField("phase", "Completed")
statusPatch.SetField("completionTime", time.Now().UTC().Format(time.RFC3339))
statusPatch.SetField("agentStatus", "idle")
statusPatch.AddCondition(conditionUpdate{Type: conditionReady, Status: "False", Reason: "Completed", Message: "Session finished"})
_ = statusPatch.Apply()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop writing status.agentStatus from the operator.

This PR moves agentStatus to query-time derivation from AG-UI events, but these terminal paths still patch it into the CR. That recreates a second source of truth in the operator: best case these writes are dead, worst case they drift from the event-log-derived value the backend/frontend now use. Let the backend derive this field and keep the CR status limited to phase/timestamps/reasons.

Also applies to: 1972-1977, 2013-2017

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/operator/internal/handlers/sessions.go` around lines 1908 - 1913,
The operator is writing status.agentStatus in several terminal paths (e.g., in
sessions.go where pod.Status.Phase == corev1.PodSucceeded and similar blocks
around the other noted ranges), which must be removed so agentStatus is derived
at query-time; remove any statusPatch.SetField("agentStatus", ...) calls (and
related writes) from those branches (including the blocks at the given ranges)
and only leave phase/timestamps/reason/conditions updates, ensuring
statusPatch.Apply() still runs where appropriate.

Makefile Outdated
Comment on lines +737 to +739
@trap 'kill 0; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \
kubectl port-forward -n ambient-code svc/frontend-service $(KIND_FWD_FRONTEND_PORT):3000 >/dev/null 2>&1 & \
kubectl port-forward -n ambient-code svc/backend-service $(KIND_FWD_BACKEND_PORT):8080 >/dev/null 2>&1 & \
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In POSIX sh/bash, what processes receive kill 0, and does it signal the current shell and the enclosing make job's process group?

💡 Result:

In POSIX, kill 0 means “send the signal to all processes in the caller’s current process group” (subject to permission checks). (man7.org) This includes the calling process itself if it’s in that group (it normally is).

So, from a shell script or a make recipe:

  • It signals the current shell process (and any other processes in the same process group). (man7.org)
    • In interactive bash, SIGTERM is ignored by default (unless you set a trap), which is why kill 0 often doesn’t “kill your interactive shell”. (linuxman7.org)
  • Whether it signals an enclosing make depends on process groups:
    • A child created by fork() inherits the parent’s process group ID, and PGID survives exec. (man7.org)
    • Therefore, a /bin/sh running a recipe will normally be in the same process group as make, so kill 0 from the recipe will typically signal make as well as the recipe’s other processes. (man7.org)

It will not signal processes outside that process group (e.g., the interactive shell that started make, which is usually in a different process group due to job control).

Citations:


🏁 Script executed:

# Check NAMESPACE definition around line 41
sed -n '35,50p' Makefile

# Search for NAMESPACE usage patterns in the Makefile
rg 'NAMESPACE' Makefile

# Check the full context of the changed lines
sed -n '730,745p' Makefile

Repository: ambient-code/platform

Length of output: 6677


Avoid kill 0 in this trap.

kill 0 signals the entire current process group, which typically includes the make process itself. On Ctrl+C this will terminate make and any sibling jobs in the same group. Capture the child PIDs and kill those explicitly instead.

Additionally, lines 738–739 hard-code ambient-code instead of using $(NAMESPACE), breaking consistency with the rest of the Makefile. This means NAMESPACE=other-ns make kind-port-forward will still target the default namespace, unlike other targets.

Proposed fix
-	`@trap` 'kill 0; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \
-	kubectl port-forward -n ambient-code svc/frontend-service $(KIND_FWD_FRONTEND_PORT):3000 >/dev/null 2>&1 & \
-	kubectl port-forward -n ambient-code svc/backend-service $(KIND_FWD_BACKEND_PORT):8080 >/dev/null 2>&1 & \
-	wait
+	`@frontend_pid`=; backend_pid=; \
+	trap 'kill $$frontend_pid $$backend_pid 2>/dev/null || true; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \
+	kubectl port-forward -n $(NAMESPACE) svc/frontend-service $(KIND_FWD_FRONTEND_PORT):3000 >/dev/null 2>&1 & frontend_pid=$$!; \
+	kubectl port-forward -n $(NAMESPACE) svc/backend-service $(KIND_FWD_BACKEND_PORT):8080 >/dev/null 2>&1 & backend_pid=$$!; \
+	wait $$frontend_pid $$backend_pid
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 737 - 739, The trap currently uses kill 0 and
hard-codes the namespace; change it to capture each background kubectl PID
(after the kubectl port-forward lines use $! to record PIDs into variables) and
in the INT trap explicitly kill those PIDs (not kill 0), then print the success
message; also replace the hard-coded "ambient-code" namespace with the Makefile
variable $(NAMESPACE) for both frontend and backend port-forward commands, and
ensure the recorded PID variables (for the frontend/backend port-forwards) are
referenced in the trap for clean shutdown.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (8)
components/operator/internal/handlers/sessions.go (1)

1908-1911: ⚠️ Potential issue | 🟠 Major

Stop patching agentStatus from monitorPod.

Elsewhere in this change set agentStatus moved to query-time derivation from the AG-UI log, but these terminal branches still write "idle" into CR status. That recreates a stale second source of truth on the operator path and is out of step with the schema removal.

Also applies to: 1972-1975, 2015-2016

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/operator/internal/handlers/sessions.go` around lines 1908 - 1911,
In monitorPod (sessions.go) remove the writes that patch agentStatus in the Pod
terminal branches: stop calling statusPatch.SetField("agentStatus", "idle") (and
similar lines at the other terminal branches around the PodSucceeded/PodFailed
handling referenced) so the operator no longer persists agentStatus; leave other
status fields (phase, completionTime) intact and rely on the query-time
derivation from the AG-UI log instead.
components/backend/websocket/agui_store.go (1)

201-256: ⚠️ Potential issue | 🟠 Major

Avoid reparsing the whole event log on every status lookup.

DeriveAgentStatus() is now on the session list/detail read path, but each call still does os.ReadFile plus per-line JSON unmarshal of the full log. As agui-events.jsonl grows, status resolution becomes O(log size) per session. Persist a compact last-status record on write and fall back to a scan only when that cache is missing or corrupt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_store.go` around lines 201 - 256,
DeriveAgentStatus currently reads and reparses agui-events.jsonl every call;
instead first attempt to read a compact cached status file (e.g.
StateBaseDir/sessions/<sessionID>/agui-status.json) and return it if valid,
falling back to the existing log scan only when the cache is missing or corrupt;
after computing status from the log, write the compact cache atomically (include
status, lastUpdated timestamp and optional runId) so future calls avoid the full
read/parse. Update the event append path that writes agui-events.jsonl (the
writer that creates tool/run events) to also update this agui-status cache when
lifecycle/tool events change status, ensuring writes are atomic and errors are
logged but do not break the main event write; keep DeriveAgentStatus’s existing
scan logic as the fallback and invalidate the cache on any write errors or
detected inconsistency.
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py (1)

124-132: ⚠️ Potential issue | 🔴 Critical

Scope halt state to the current run, not the shared adapter.

ClaudeBridge caches a single self._adapter across thread IDs, so self._adapter.halted is shared mutable state. Another concurrent run can overwrite that flag before this check executes, which can miss the interrupt or interrupt the wrong worker. Capture the halt result from the current adapter invocation or key halt bookkeeping by thread_id.

Verify that halt state is stored per-run/thread rather than on the bridge-wide adapter instance. The concern is confirmed if self._adapter is singleton-scoped and halt bookkeeping is not keyed by thread/session.

#!/bin/bash
set -euo pipefail

echo "== Adapter lifetime =="
rg -n -C2 'self\._adapter|ClaudeAgentAdapter' components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py

echo
echo "== Halt bookkeeping =="
rg -n -C2 '\.halted\b|_halted_tool_call_id' components/runners/ambient-runner/ambient_runner
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 124 - 132, The check uses the bridge-wide self._adapter.halted
which is shared across runs and can race; instead scope halt state to the
current run by reading the halted flag from the adapter instance used for this
thread or by maintaining per-thread halt bookkeeping keyed by thread_id.
Concretely: in the ClaudeBridge flow where you call into the adapter (the same
area that later checks self._adapter.halted and calls worker.interrupt), capture
the adapter returned/used for this invocation into a local variable (e.g.,
adapter = self._adapter or from whatever _get_adapter/_ensure_adapter method is
used) and use adapter.halted for the conditional, or introduce a
self._halted_by_thread dict and set/clear self._halted_by_thread[thread_id] when
the adapter reports a halt and read that keyed value before calling
worker.interrupt; update any adapter creation/teardown paths to initialize/clear
the per-thread entry.
components/frontend/src/components/ui/ask-user-question.tsx (3)

275-289: ⚠️ Potential issue | 🟡 Minor

Add explicit type="button" to prevent accidental form submission.

The tab navigation buttons lack an explicit type attribute and will default to type="submit", which can trigger unintended form submissions.

🐛 Proposed fix
                     <button
+                      type="button"
                       key={idx}
                       onClick={() => setActiveTab(idx)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/ask-user-question.tsx` around lines 275
- 289, The tab buttons currently default to submit and can trigger form
submission; update the button element (the one with onClick={() =>
setActiveTab(idx)} and rendering CheckCircle2 and {q.header || `Q${idx + 1}`})
to include an explicit type="button" attribute to prevent accidental form
submissions.

18-24: ⚠️ Potential issue | 🟠 Major

Await the answer send before locking the question.

The onSubmitAnswer callback is typed as synchronous void, and handleSubmit immediately sets submitted = true without awaiting. If the network send fails, the widget becomes locked with no retry path.

🐛 Proposed fix
 export type AskUserQuestionMessageProps = {
   toolUseBlock: ToolUseBlock;
   resultBlock?: ToolResultBlock;
   timestamp?: string;
-  onSubmitAnswer?: (formattedAnswer: string) => void;
+  onSubmitAnswer?: (formattedAnswer: string) => Promise<void>;
   isNewest?: boolean;
 };
+  const [isSubmitting, setIsSubmitting] = useState(false);
-  const disabled = alreadyAnswered || submitted || !isNewest;
+  const disabled = alreadyAnswered || submitted || isSubmitting || !isNewest;

-  const handleSubmit = () => {
+  const handleSubmit = async () => {
     if (!onSubmitAnswer || !allQuestionsAnswered || disabled) return;

     const answers: Record<string, string> = {};
     for (const q of questions) {
       const sel = selections[q.question];
       answers[q.question] = Array.isArray(sel) ? sel.join(", ") : (sel as string);
     }

     const response = JSON.stringify({ questions, answers });
-    onSubmitAnswer(response);
-    setSubmitted(true);
+    try {
+      setIsSubmitting(true);
+      await onSubmitAnswer(response);
+      setSubmitted(true);
+    } finally {
+      setIsSubmitting(false);
+    }
   };

Also applies to: 108-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/ask-user-question.tsx` around lines 18
- 24, The submit flow marks the question locked immediately because
onSubmitAnswer is typed as synchronous void and handleSubmit sets submitted =
true without awaiting; update AskUserQuestionMessageProps so onSubmitAnswer
returns Promise<void> (or Promise<boolean> if you want success info), change
handleSubmit to await onSubmitAnswer(...) and only set submitted = true after
the awaited call succeeds, and add a try/catch around the await to
handle/rethrow/log errors and keep the widget unlocked on failure so the user
can retry; locate these changes in the AskUserQuestionMessageProps type and the
handleSubmit function within ask-user-question.tsx.

202-230: ⚠️ Potential issue | 🟡 Minor

Add keyboard support to the "Other" option for accessibility.

The "Other" option uses onClick without corresponding keyboard event handlers (onKeyDown/onKeyUp), making it inaccessible to keyboard-only users. This violates a11y requirements flagged by Biome.

♿ Proposed fix
         <label
           className={cn(
             "flex items-center gap-2.5 p-1.5 rounded cursor-pointer transition-colors",
             isOther ? "bg-accent" : "hover:bg-muted/50",
             disabled && "cursor-default opacity-60"
           )}
           onClick={() => !disabled && handleOtherToggle(q.question)}
+          onKeyDown={(e) => {
+            if (!disabled && (e.key === "Enter" || e.key === " ")) {
+              e.preventDefault();
+              handleOtherToggle(q.question);
+            }
+          }}
+          tabIndex={disabled ? -1 : 0}
+          role="button"
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/ask-user-question.tsx` around lines 202
- 230, The "Other" label currently only handles mouse clicks via onClick and is
not keyboard-accessible; update the label wrapper to be keyboard-focusable (add
tabIndex={0} and role="button") and add an onKeyDown handler that calls
handleOtherToggle(q.question) when Enter or Space is pressed (respecting the
disabled flag), ensuring the same disabled-check used in the existing onClick is
applied; keep the existing Input onClick stopPropagation and ensure the added
onKeyDown does not interfere with focus when the inner Input is rendered.
components/frontend/src/components/session/MessagesTab.tsx (1)

143-152: ⚠️ Potential issue | 🟠 Major

Use stable IDs instead of array indices for message keys to preserve component state.

When showSystemMessages toggles, the filteredMessages array changes shape. Index-based keys (sm-${idx}) cause StreamMessage components to remount, which will reset AskUserQuestionMessage local state (selections, tabs, freeform input).

🐛 Proposed fix
-        {shouldShowMessages && filteredMessages.map((m, idx) => (
-          <StreamMessage
-            key={`sm-${idx}`}
-            message={m}
-            isNewest={idx === filteredMessages.length - 1}
-            onGoToResults={onGoToResults}
-            agentName={agentName}
-            onSubmitAnswer={onSendToolAnswer}
-          />
-        ))}
+        {shouldShowMessages && filteredMessages.map((m, idx) => {
+          const messageKey =
+            "toolUseBlock" in m ? `tool-${m.toolUseBlock.id}` : `msg-${m.id ?? idx}`;
+          return (
+            <StreamMessage
+              key={messageKey}
+              message={m}
+              isNewest={idx === filteredMessages.length - 1}
+              onGoToResults={onGoToResults}
+              agentName={agentName}
+              onSubmitAnswer={onSendToolAnswer}
+            />
+          );
+        })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/MessagesTab.tsx` around lines 143
- 152, The keys for StreamMessage are unstable because they use array indices
(sm-${idx}); update MessagesTab to use a stable unique identifier from each
message object (e.g., m.id or another persistent field like
m.messageId/timestamp) as the key so StreamMessage instances (and nested
AskUserQuestionMessage state) are preserved across filteredMessages shape
changes; locate the JSX mapping in MessagesTab where filteredMessages.map
produces StreamMessage and replace the key generation to reference the message's
stable id field, ensuring all messages have that id or fallback to a stable
composite key derived from message properties.
components/backend/handlers/sessions.go (1)

367-384: ⚠️ Potential issue | 🟠 Major

Missing namespace in agent status derivation call.

This function extracts only the session name but not the namespace. To fix the cross-project collision issue, extract and pass the namespace:

🔧 Suggested fix
 func enrichAgentStatus(session *types.AgenticSession) {
 	if session.Status == nil || session.Status.Phase != "Running" {
 		return
 	}
 	if DeriveAgentStatusFromEvents == nil {
 		return
 	}
 	name, _ := session.Metadata["name"].(string)
+	namespace, _ := session.Metadata["namespace"].(string)
-	if name == "" {
+	if name == "" || namespace == "" {
 		return
 	}
-	if derived := DeriveAgentStatusFromEvents(name); derived != "" {
+	if derived := DeriveAgentStatusFromEvents(namespace, name); derived != "" {
 		session.Status.AgentStatus = types.StringPtr(derived)
 	}
 }

This requires updating DeriveAgentStatusFromEvents signature and the underlying websocket.DeriveAgentStatus implementation to accept namespace.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 367 - 384,
enrichAgentStatus currently only extracts the session name and calls
DeriveAgentStatusFromEvents(name), causing cross-project collisions; update
enrichAgentStatus (in components/backend/handlers/sessions.go) to also extract
namespace from session.Metadata["namespace"] (string), validate it, and call the
updated DeriveAgentStatusFromEvents(namespace, name) signature; update the
global function type/signature for DeriveAgentStatusFromEvents and its
implementation websocket.DeriveAgentStatus to accept (namespace, name) and
propagate the namespace change to any other callers so derived AgentStatus is
namespaced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/backend/websocket/agui_store.go`:
- Around line 201-209: DeriveAgentStatus currently reads agui-events.jsonl
directly (using StateBaseDir and os.ReadFile) which bypasses the
migration/loader for messages.jsonl → agui-events.jsonl and causes legacy
sessions to return "". Replace the direct file read with the canonical session
events loader/migration path used elsewhere (the function that guarantees
messages.jsonl is migrated to agui-events.jsonl—the same loader used by the
/agui/events replay code), then derive status from the loader's returned events
(instead of splitLines on raw file bytes); keep references to DeriveAgentStatus,
StateBaseDir and splitLines in your changes so the logic stays clear.

In `@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/page.tsx:
- Around line 1390-1399: The sendToolAnswer function swallows errors by catching
and only showing a toast, preventing callers (e.g., AskUserQuestionMessage) from
detecting failures; update sendToolAnswer (which calls aguiSendMessage) to
re-throw the caught error (or explicitly return a rejected Promise) after
displaying the toast so upstream UI can handle the failure state, e.g., wrap
aguiSendMessage in try/catch, call toast.error(...), then throw err to propagate
the error to the caller.

In `@components/frontend/src/components/agent-status-indicator.tsx`:
- Around line 25-105: The switch on status inside the AgentStatusIndicator
component currently handles known AgentStatus values but has no default branch;
add an exhaustive check so TypeScript will error when new AgentStatus values are
added. In the switch(status) block add a final default that asserts
exhaustiveness (e.g. assign status to a never-typed variable or call an
assertNever helper) and return null or throw — referencing the symbols:
AgentStatus (type), status (switch variable), and the
AgentStatusIndicator/component switch block so the compiler enforces handling of
any new statuses.

In `@components/frontend/src/hooks/agui/types.ts`:
- Line 28: The sendMessage signature currently uses Record<string, unknown>
which allows non-JSON-serializable values; define a reusable
JsonValue/JsonObject type (e.g., type JsonValue = string | number | boolean |
null | JsonObject | JsonValue[]; type JsonObject = { [key: string]: JsonValue })
and replace the metadata parameter type in sendMessage with JsonObject |
undefined (and export the types if needed) so callers are constrained to
JSON-serializable values before JSON.stringify is called; update any related
imports/exports in components/frontend/src/hooks/agui/types.ts to use the new
JsonObject type.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 919-923: After setting self._halted = True and
self._halted_tool_call_id = current_tool_call_id and before continue (where
halt_event_stream = True), clear the in-flight tool-call state so the finally
block doesn't emit a duplicate ToolCallEndEvent for current_tool_call_id;
specifically set current_tool_call_id (and any related in-progress flags/state
used to decide emitting ToolCallEndEvent) to None/false so the subsequent
ToolCallEndEvent emission logic sees no active tool call.

---

Duplicate comments:
In `@components/backend/handlers/sessions.go`:
- Around line 367-384: enrichAgentStatus currently only extracts the session
name and calls DeriveAgentStatusFromEvents(name), causing cross-project
collisions; update enrichAgentStatus (in
components/backend/handlers/sessions.go) to also extract namespace from
session.Metadata["namespace"] (string), validate it, and call the updated
DeriveAgentStatusFromEvents(namespace, name) signature; update the global
function type/signature for DeriveAgentStatusFromEvents and its implementation
websocket.DeriveAgentStatus to accept (namespace, name) and propagate the
namespace change to any other callers so derived AgentStatus is namespaced.

In `@components/backend/websocket/agui_store.go`:
- Around line 201-256: DeriveAgentStatus currently reads and reparses
agui-events.jsonl every call; instead first attempt to read a compact cached
status file (e.g. StateBaseDir/sessions/<sessionID>/agui-status.json) and return
it if valid, falling back to the existing log scan only when the cache is
missing or corrupt; after computing status from the log, write the compact cache
atomically (include status, lastUpdated timestamp and optional runId) so future
calls avoid the full read/parse. Update the event append path that writes
agui-events.jsonl (the writer that creates tool/run events) to also update this
agui-status cache when lifecycle/tool events change status, ensuring writes are
atomic and errors are logged but do not break the main event write; keep
DeriveAgentStatus’s existing scan logic as the fallback and invalidate the cache
on any write errors or detected inconsistency.

In `@components/frontend/src/components/session/MessagesTab.tsx`:
- Around line 143-152: The keys for StreamMessage are unstable because they use
array indices (sm-${idx}); update MessagesTab to use a stable unique identifier
from each message object (e.g., m.id or another persistent field like
m.messageId/timestamp) as the key so StreamMessage instances (and nested
AskUserQuestionMessage state) are preserved across filteredMessages shape
changes; locate the JSX mapping in MessagesTab where filteredMessages.map
produces StreamMessage and replace the key generation to reference the message's
stable id field, ensuring all messages have that id or fallback to a stable
composite key derived from message properties.

In `@components/frontend/src/components/ui/ask-user-question.tsx`:
- Around line 275-289: The tab buttons currently default to submit and can
trigger form submission; update the button element (the one with onClick={() =>
setActiveTab(idx)} and rendering CheckCircle2 and {q.header || `Q${idx + 1}`})
to include an explicit type="button" attribute to prevent accidental form
submissions.
- Around line 18-24: The submit flow marks the question locked immediately
because onSubmitAnswer is typed as synchronous void and handleSubmit sets
submitted = true without awaiting; update AskUserQuestionMessageProps so
onSubmitAnswer returns Promise<void> (or Promise<boolean> if you want success
info), change handleSubmit to await onSubmitAnswer(...) and only set submitted =
true after the awaited call succeeds, and add a try/catch around the await to
handle/rethrow/log errors and keep the widget unlocked on failure so the user
can retry; locate these changes in the AskUserQuestionMessageProps type and the
handleSubmit function within ask-user-question.tsx.
- Around line 202-230: The "Other" label currently only handles mouse clicks via
onClick and is not keyboard-accessible; update the label wrapper to be
keyboard-focusable (add tabIndex={0} and role="button") and add an onKeyDown
handler that calls handleOtherToggle(q.question) when Enter or Space is pressed
(respecting the disabled flag), ensuring the same disabled-check used in the
existing onClick is applied; keep the existing Input onClick stopPropagation and
ensure the added onKeyDown does not interfere with focus when the inner Input is
rendered.

In `@components/operator/internal/handlers/sessions.go`:
- Around line 1908-1911: In monitorPod (sessions.go) remove the writes that
patch agentStatus in the Pod terminal branches: stop calling
statusPatch.SetField("agentStatus", "idle") (and similar lines at the other
terminal branches around the PodSucceeded/PodFailed handling referenced) so the
operator no longer persists agentStatus; leave other status fields (phase,
completionTime) intact and rely on the query-time derivation from the AG-UI log
instead.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 124-132: The check uses the bridge-wide self._adapter.halted which
is shared across runs and can race; instead scope halt state to the current run
by reading the halted flag from the adapter instance used for this thread or by
maintaining per-thread halt bookkeeping keyed by thread_id. Concretely: in the
ClaudeBridge flow where you call into the adapter (the same area that later
checks self._adapter.halted and calls worker.interrupt), capture the adapter
returned/used for this invocation into a local variable (e.g., adapter =
self._adapter or from whatever _get_adapter/_ensure_adapter method is used) and
use adapter.halted for the conditional, or introduce a self._halted_by_thread
dict and set/clear self._halted_by_thread[thread_id] when the adapter reports a
halt and read that keyed value before calling worker.interrupt; update any
adapter creation/teardown paths to initialize/clear the per-thread entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4698331c-9687-48ab-bb59-a5865fe6b316

📥 Commits

Reviewing files that changed from the base of the PR and between cb1c8c2 and 56e76ff.

⛔ Files ignored due to path filters (1)
  • components/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • .gitignore
  • Makefile
  • components/backend/handlers/sessions.go
  • components/backend/main.go
  • components/backend/types/agui.go
  • components/backend/types/session.go
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
  • components/frontend/src/components/agent-status-indicator.tsx
  • components/frontend/src/components/session-status-dot.tsx
  • components/frontend/src/components/session/MessagesTab.tsx
  • components/frontend/src/components/ui/ask-user-question.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/components/ui/tool-message.tsx
  • components/frontend/src/components/workspace-sections/sessions-section.tsx
  • components/frontend/src/hooks/agui/types.ts
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/hooks/use-agui-stream.ts
  • components/frontend/src/services/queries/use-sessions.ts
  • components/frontend/src/types/agentic-session.ts
  • components/frontend/src/types/api/sessions.ts
  • components/operator/internal/handlers/reconciler.go
  • components/operator/internal/handlers/sessions.go
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/ambient_runner/platform/prompts.py

Comment on lines +201 to +209
func DeriveAgentStatus(sessionID string) string {
path := fmt.Sprintf("%s/sessions/%s/agui-events.jsonl", StateBaseDir, sessionID)

data, err := os.ReadFile(path)
if err != nil {
return ""
}

lines := splitLines(data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep status reads on the same migration path as event replay.

This function bypasses the loader that handles messages.jsonlagui-events.jsonl migration, so legacy sessions will resolve to "" here until someone opens /agui/events. Status derivation should share the same loading/migration path as replay.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_store.go` around lines 201 - 209,
DeriveAgentStatus currently reads agui-events.jsonl directly (using StateBaseDir
and os.ReadFile) which bypasses the migration/loader for messages.jsonl →
agui-events.jsonl and causes legacy sessions to return "". Replace the direct
file read with the canonical session events loader/migration path used elsewhere
(the function that guarantees messages.jsonl is migrated to
agui-events.jsonl—the same loader used by the /agui/events replay code), then
derive status from the loader's returned events (instead of splitLines on raw
file bytes); keep references to DeriveAgentStatus, StateBaseDir and splitLines
in your changes so the logic stays clear.

connect: (runId?: string) => void
disconnect: () => void
sendMessage: (content: string) => Promise<void>
sendMessage: (content: string, metadata?: Record<string, unknown>) => Promise<void>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'types.ts' components/frontend/src | xargs -r rg -n -C2 '\b(JsonValue|JsonObject|metadata)\b'
rg -n -C2 'sendMessage:\s*\(content: string, metadata\?:' components/frontend/src

Repository: ambient-code/platform

Length of output: 940


🏁 Script executed:

cat -n components/frontend/src/hooks/use-agui-stream.ts | head -100

Repository: ambient-code/platform

Length of output: 3867


🏁 Script executed:

rg -n 'JSON\.stringify|JSON\.parse' components/frontend/src --type ts --type tsx -C 2

Repository: ambient-code/platform

Length of output: 92


🏁 Script executed:

rg -n '\btype\s+Json|interface\s+Json' components/frontend --type ts --type tsx

Repository: ambient-code/platform

Length of output: 92


🏁 Script executed:

wc -l components/frontend/src/hooks/use-agui-stream.ts

Repository: ambient-code/platform

Length of output: 117


🏁 Script executed:

tail -n 200 components/frontend/src/hooks/use-agui-stream.ts

Repository: ambient-code/platform

Length of output: 6280


🏁 Script executed:

rg 'JSON\.stringify' components/frontend/src -C 3

Repository: ambient-code/platform

Length of output: 50377


🏁 Script executed:

rg 'sendMessage' components/frontend/src -C 5

Repository: ambient-code/platform

Length of output: 8929


Constrain metadata to JSON-serializable values.

The public type Record<string, unknown> is wider than the transport contract. The sendMessage callback passes metadata into JSON.stringify(), but the current type accepts values like bigint, Date, Function, and class instances—which will silently fail or be omitted during serialization. Define and reuse a JsonObject type to prevent callers from passing invalid values.

Proposed fix
+export type JsonPrimitive = string | number | boolean | null
+export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
+export type JsonObject = { [key: string]: JsonValue }
+
 export type UseAGUIStreamReturn = {
   state: AGUIClientState
   connect: (runId?: string) => void
   disconnect: () => void
-  sendMessage: (content: string, metadata?: Record<string, unknown>) => Promise<void>
+  sendMessage: (content: string, metadata?: JsonObject) => Promise<void>
   interrupt: () => Promise<void>
   isConnected: boolean
   isStreaming: boolean
   isRunActive: boolean
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sendMessage: (content: string, metadata?: Record<string, unknown>) => Promise<void>
export type JsonPrimitive = string | number | boolean | null
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
export type JsonObject = { [key: string]: JsonValue }
export type UseAGUIStreamReturn = {
state: AGUIClientState
connect: (runId?: string) => void
disconnect: () => void
sendMessage: (content: string, metadata?: JsonObject) => Promise<void>
interrupt: () => Promise<void>
isConnected: boolean
isStreaming: boolean
isRunActive: boolean
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/hooks/agui/types.ts` at line 28, The sendMessage
signature currently uses Record<string, unknown> which allows
non-JSON-serializable values; define a reusable JsonValue/JsonObject type (e.g.,
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
type JsonObject = { [key: string]: JsonValue }) and replace the metadata
parameter type in sendMessage with JsonObject | undefined (and export the types
if needed) so callers are constrained to JSON-serializable values before
JSON.stringify is called; update any related imports/exports in
components/frontend/src/hooks/agui/types.ts to use the new JsonObject type.

@mergify
Copy link

mergify bot commented Mar 10, 2026

🧪 CI Insights

Here's what we observed from your CI run for 2075dcb.

🟢 All jobs passed!

But CI Insights is watching 👀

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
components/frontend/src/components/session/ask-user-question.tsx (1)

18-24: ⚠️ Potential issue | 🟠 Major

Type mismatch: onSubmitAnswer should return Promise<void> to match upstream.

The prop is typed as (formattedAnswer: string) => void, but MessagesTabProps.onSendToolAnswer is typed as (formattedAnswer: string) => Promise<void>. This mismatch prevents proper async error handling in handleSubmit.

Suggested fix
 export type AskUserQuestionMessageProps = {
   toolUseBlock: ToolUseBlock;
   resultBlock?: ToolResultBlock;
   timestamp?: string;
-  onSubmitAnswer?: (formattedAnswer: string) => void;
+  onSubmitAnswer?: (formattedAnswer: string) => Promise<void>;
   isNewest?: boolean;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/ask-user-question.tsx` around
lines 18 - 24, The prop type onSubmitAnswer in AskUserQuestionMessageProps is
currently synchronous but must be async to match
MessagesTabProps.onSendToolAnswer; change the type of onSubmitAnswer to
(formattedAnswer: string) => Promise<void>, update any usages (e.g., the
handleSubmit function inside this component) to await the call and
return/propagate the Promise so errors can be handled upstream, and ensure
callers (who implement MessagesTabProps.onSendToolAnswer) remain compatible with
the new signature; reference AskUserQuestionMessageProps, onSubmitAnswer,
handleSubmit, and MessagesTabProps.onSendToolAnswer when making the change.
components/backend/websocket/agui_store.go (1)

198-248: ⚠️ Potential issue | 🟡 Minor

Legacy session migration not triggered — status will be empty until event replay.

DeriveAgentStatus reads agui-events.jsonl directly (line 203) but does not call MigrateLegacySessionToAGUI like loadEvents does (lines 170-173). For legacy sessions that only have messages.jsonl, this function will return "" until the user opens /agui/events (which triggers migration via loadEvents).

Consider either:

  1. Calling migration before the file read, or
  2. Documenting this as intentional behavior (migration happens on first event replay)
🔧 Proposed fix to trigger migration
 func DeriveAgentStatus(sessionID string) string {
 	path := fmt.Sprintf("%s/sessions/%s/agui-events.jsonl", StateBaseDir, sessionID)
 
+	// Trigger migration for legacy sessions (messages.jsonl → agui-events.jsonl)
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		_ = MigrateLegacySessionToAGUI(sessionID)
+	}
+
 	// Read only the tail of the file to avoid loading entire event log into memory.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_store.go` around lines 198 - 248,
DeriveAgentStatus reads agui-events.jsonl directly and therefore returns empty
for legacy sessions that only have messages.jsonl; call
MigrateLegacySessionToAGUI(sessionID) at the start of DeriveAgentStatus (before
opening/reading the file) to ensure legacy sessions are migrated to
agui-events.jsonl—mirror what loadEvents does—so the event log exists for status
derivation; keep existing error handling if migration fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/components/session/ask-user-question.tsx`:
- Around line 141-164: The label elements currently rely on implicit nesting for
association which triggers lint/a11y/noLabelWithoutControl; fix by giving each
control an explicit id and referencing it from the label via htmlFor: generate a
stable unique id per option (e.g., based on q.question + opt.label sanitized)
and pass that id to the Checkbox (and to RadioGroupItem for single-select) and
set the label's htmlFor to the same id so the association is explicit; update
places that render Checkbox and RadioGroupItem (and any handlers like
handleMultiSelect usage) to accept/preserve the id but do not change their
behavior otherwise.

In `@components/frontend/src/components/ui/stream-message.tsx`:
- Around line 13-20: StreamMessageProps currently types onSubmitAnswer as
(formattedAnswer: string) => void but the actual handler
MessagesTabProps.onSendToolAnswer returns Promise<void>; update the
StreamMessageProps type signature for onSubmitAnswer to (formattedAnswer:
string) => Promise<void> so the returned Promise is preserved and callers can
await/errors can be propagated (search for StreamMessageProps and the
onSubmitAnswer usage to update any related calls/signatures to match).

In `@components/frontend/src/types/api/sessions.ts`:
- Around line 32-34: The type StoredAgentStatus is duplicated in two places;
remove the duplicate definition from the sessions module and re-export the
single source of truth from the other module instead: keep StoredAgentStatus
defined only in the agentic-session module and in the sessions module replace
the local definition with an export/re-export that forwards StoredAgentStatus
from the agentic-session symbol so all references use the single type.

---

Duplicate comments:
In `@components/backend/websocket/agui_store.go`:
- Around line 198-248: DeriveAgentStatus reads agui-events.jsonl directly and
therefore returns empty for legacy sessions that only have messages.jsonl; call
MigrateLegacySessionToAGUI(sessionID) at the start of DeriveAgentStatus (before
opening/reading the file) to ensure legacy sessions are migrated to
agui-events.jsonl—mirror what loadEvents does—so the event log exists for status
derivation; keep existing error handling if migration fails.

In `@components/frontend/src/components/session/ask-user-question.tsx`:
- Around line 18-24: The prop type onSubmitAnswer in AskUserQuestionMessageProps
is currently synchronous but must be async to match
MessagesTabProps.onSendToolAnswer; change the type of onSubmitAnswer to
(formattedAnswer: string) => Promise<void>, update any usages (e.g., the
handleSubmit function inside this component) to await the call and
return/propagate the Promise so errors can be handled upstream, and ensure
callers (who implement MessagesTabProps.onSendToolAnswer) remain compatible with
the new signature; reference AskUserQuestionMessageProps, onSubmitAnswer,
handleSubmit, and MessagesTabProps.onSendToolAnswer when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f23619f2-2854-42ce-ab34-35989f1d5917

📥 Commits

Reviewing files that changed from the base of the PR and between 56e76ff and 2075dcb.

📒 Files selected for processing (8)
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/backend/websocket/agui_store_test.go
  • components/frontend/src/components/session/MessagesTab.tsx
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/types/agentic-session.ts
  • components/frontend/src/types/api/sessions.ts

Comment on lines +202 to +248
func DeriveAgentStatus(sessionID string) string {
path := fmt.Sprintf("%s/sessions/%s/agui-events.jsonl", StateBaseDir, sessionID)

// Read only the tail of the file to avoid loading entire event log into memory.
// 64KB is sufficient for recent lifecycle events (scanning backwards).
const maxTailBytes = 64 * 1024

file, err := os.Open(path)
if err != nil {
return ""
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
return ""
}

fileSize := stat.Size()
var data []byte

if fileSize <= maxTailBytes {
// File is small, read it all
data, err = os.ReadFile(path)
if err != nil {
return ""
}
} else {
// File is large, seek to tail and read last N bytes
offset := fileSize - maxTailBytes
_, err = file.Seek(offset, 0)
if err != nil {
return ""
}

data = make([]byte, maxTailBytes)
n, err := file.Read(data)
if err != nil {
return ""
}
data = data[:n]

// Skip partial first line (we seeked into the middle of a line)
if idx := bytes.IndexByte(data, '\n'); idx >= 0 {
data = data[idx+1:]
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider caching derived status to reduce I/O on list endpoints.

The 64KB tail read optimization is good, but DeriveAgentStatus is called for every "Running" session in ListSessions (see enrichAgentStatus in sessions.go). Each call performs:

  • os.Open + file.Stat + file.Seek + file.Read
  • JSON parsing of each line in the tail

For endpoints listing many sessions, this creates per-session I/O overhead. Consider caching the derived status (with TTL or invalidation on new events) to avoid repeated file reads for the same session within a short window.

Comment on lines +141 to +164
<label
key={opt.label}
className={cn(
"flex gap-2.5 p-1.5 rounded cursor-pointer transition-colors",
opt.description ? "items-start" : "items-center",
isSelected ? "bg-accent" : "hover:bg-muted/50",
disabled && "cursor-default opacity-60"
)}
>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
handleMultiSelect(q.question, opt.label, checked === true)
}
disabled={disabled}
className={opt.description ? "mt-1" : ""}
/>
<div className="min-w-0">
<span className="text-sm leading-5">{opt.label}</span>
{opt.description && (
<p className="text-xs text-muted-foreground leading-tight mt-0.5">{opt.description}</p>
)}
</div>
</label>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Static analysis flags: labels not associated with form controls.

Biome reports lint/a11y/noLabelWithoutControl for the <label> elements wrapping Checkbox (lines 141-164) and RadioGroupItem (lines 183-199). While these work visually because the input is a child, the association is implicit. Consider adding htmlFor and id attributes for explicit association, or verify that implicit nesting satisfies project accessibility requirements.

Also applies to: 183-199

🧰 Tools
🪛 Biome (2.4.6)

[error] 141-164: A form label must be associated with an input.

(lint/a11y/noLabelWithoutControl)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/ask-user-question.tsx` around
lines 141 - 164, The label elements currently rely on implicit nesting for
association which triggers lint/a11y/noLabelWithoutControl; fix by giving each
control an explicit id and referencing it from the label via htmlFor: generate a
stable unique id per option (e.g., based on q.question + opt.label sanitized)
and pass that id to the Checkbox (and to RadioGroupItem for single-select) and
set the label's htmlFor to the same id so the association is explicit; update
places that render Checkbox and RadioGroupItem (and any handlers like
handleMultiSelect usage) to accept/preserve the id but do not change their
behavior otherwise.

Comment on lines +32 to +34
// Subset of agent status values that can be persisted in the CR status field
// (completed/failed are derived at query time from phase, not stored)
export type StoredAgentStatus = "working" | "idle" | "waiting_input";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Duplicate type definition — consider re-exporting from a single source.

StoredAgentStatus is defined identically in both types/api/sessions.ts and types/agentic-session.ts. This creates a maintenance burden if the values need to change.

Consider defining the type in one location and re-exporting it from the other:

// In api/sessions.ts
export type { StoredAgentStatus } from "@/types/agentic-session";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/types/api/sessions.ts` around lines 32 - 34, The type
StoredAgentStatus is duplicated in two places; remove the duplicate definition
from the sessions module and re-export the single source of truth from the other
module instead: keep StoredAgentStatus defined only in the agentic-session
module and in the sessions module replace the local definition with an
export/re-export that forwards StoredAgentStatus from the agentic-session symbol
so all references use the single type.

@Gkrumbach07
Copy link
Contributor Author

@ambient-fix

@ambient-code ambient-code bot force-pushed the feat/human-in-the-loop branch from 2075dcb to 957bc96 Compare March 11, 2026 13:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (13)
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py (1)

127-132: ⚠️ Potential issue | 🔴 Critical

Don’t read halt state from the shared adapter here.

Line 127 still gates the interrupt on self._adapter.halted, but components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py:237-250 exposes that as a single _halted flag and components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py:910-920 mutates it during a run. Since ClaudeBridge reuses one adapter across thread-scoped workers, concurrent runs can overwrite that flag before this branch executes, so you can miss the current halt or interrupt the wrong worker. Capture the halt outcome from the specific run() invocation, or key halt state by thread_id instead of reading bridge-wide adapter state here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 127 - 132, The interrupt check in ClaudeBridge currently reads the
bridge-wide flag self._adapter.halted which can be overwritten by concurrent
runs; instead capture the halt outcome for this specific run (e.g., return a
halted boolean from adapter.run(...) or have adapter.record_halt(thread_id) and
read that per-thread key) and use that value to decide whether to call
worker.interrupt(); update the ClaudeBridge logic (the branch checking
self._adapter.halted before awaiting worker.interrupt()) to use the run-specific
halted result or per-thread halted map keyed by thread_id so concurrent runs
cannot clobber each other.
Makefile (1)

737-740: ⚠️ Potential issue | 🟠 Major

Still unresolved: avoid kill 0 and the hard-coded namespace here.

kill 0 can tear down the whole recipe process group, including the enclosing make, and -n ambient-code makes NAMESPACE=... make kind-port-forward forward the wrong services. Capture the two kubectl PIDs, kill those explicitly in the trap, and use $(NAMESPACE) for both commands.

Proposed fix
-	`@trap` 'kill 0; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \
-	kubectl port-forward -n ambient-code svc/frontend-service $(KIND_FWD_FRONTEND_PORT):3000 >/dev/null 2>&1 & \
-	kubectl port-forward -n ambient-code svc/backend-service $(KIND_FWD_BACKEND_PORT):8080 >/dev/null 2>&1 & \
-	wait
+	`@frontend_pid`=; backend_pid=; \
+	trap 'kill $$frontend_pid $$backend_pid 2>/dev/null || true; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \
+	kubectl port-forward -n $(NAMESPACE) svc/frontend-service $(KIND_FWD_FRONTEND_PORT):3000 >/dev/null 2>&1 & frontend_pid=$$!; \
+	kubectl port-forward -n $(NAMESPACE) svc/backend-service $(KIND_FWD_BACKEND_PORT):8080 >/dev/null 2>&1 & backend_pid=$$!; \
+	wait $$frontend_pid $$backend_pid

Run this read-only check to confirm the target still contains both problems and that NAMESPACE is configurable elsewhere:

#!/bin/bash
sed -n '729,740p' Makefile
echo '---'
rg -n '^[[:space:]]*NAMESPACE[[:space:]]*\?=|kill 0|port-forward -n ambient-code' Makefile
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 737 - 740, The trap currently uses `kill 0` and the
port-forward lines hard-code `ambient-code`; change the recipe to capture the
two background kubectl PIDs (e.g., start each `kubectl port-forward -n
$(NAMESPACE) svc/... ... &` and save their $$! values into variables like
FRONTEND_PID and BACKEND_PID), update both `-n ambient-code` occurrences to `-n
$(NAMESPACE)`, and in the trap kill those specific PIDs (kill -TERM
$(FRONTEND_PID) $(BACKEND_PID) || true) and wait for them instead of `kill 0`;
keep the existing redirection and `wait` behavior so the foreground Make job
still blocks until both forwards exit.
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py (1)

919-923: ⚠️ Potential issue | 🔴 Critical

Duplicate TOOL_CALL_END emitted after frontend tool halt.

After setting the halt state and continue, the loop breaks at line 680, entering the finally block. Since current_tool_call_id is not cleared, line 1085 emits a second ToolCallEndEvent for the same tool call.

This violates the AG-UI tool call sequence: TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END (exactly once per tool call).

,

🐛 Proposed fix: Clear tool call state after setting halt flags
                                self._halted = True
                                self._halted_tool_call_id = current_tool_call_id
                                halt_event_stream = True
+
+                                # Clear tool state to avoid duplicate TOOL_CALL_END in finally block
+                                current_tool_call_id = None
+                                current_tool_call_name = None
+                                current_tool_display_name = None
+                                accumulated_tool_json = ""
+
                                 # Continue consuming remaining events for cleanup
                                 continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py` around lines
919 - 923, When you set self._halted = True and self._halted_tool_call_id =
current_tool_call_id inside the loop, also clear the in-progress tool-call state
(e.g., set current_tool_call_id = None or clear the attribute that tracks the
active tool call) before using continue so that the finally block in the
surrounding function does not emit a second ToolCallEndEvent; update the same
block where you set self._halted/self._halted_tool_call_id and ensure the
finally clause that emits ToolCallEndEvent checks that current_tool_call_id (or
the instance attribute tracking the active tool call) is not None and matches
expected state before emitting.
components/frontend/src/components/session/ask-user-question.tsx (2)

278-292: ⚠️ Potential issue | 🟡 Minor

Add type="button" to tab navigation buttons.

These buttons lack an explicit type attribute and will default to type="submit", causing unintended form submissions if this component is used inside a form.

🔧 Proposed fix
                     <button
+                      type="button"
                       key={idx}
                       onClick={() => setActiveTab(idx)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/ask-user-question.tsx` around
lines 278 - 292, The tab buttons in the render loop (the <button> that calls
setActiveTab(idx) and conditionally renders CheckCircle2 and
q.header/`Q${idx+1}`) are missing an explicit type and will default to submit
inside forms; update that JSX button element to include type="button" so clicks
do not trigger form submission, leaving the onClick, className logic, and
conditional children unchanged.

111-125: ⚠️ Potential issue | 🟠 Major

Await the answer submission before marking as submitted.

handleSubmit calls onSubmitAnswer and immediately sets submitted = true. If the network send fails, the question UI remains locked with no retry path. Make onSubmitAnswer return Promise<void>, add loading state, and only mark submitted on success.

🔧 Proposed fix
 export type AskUserQuestionMessageProps = {
   toolUseBlock: ToolUseBlock;
   resultBlock?: ToolResultBlock;
   timestamp?: string;
-  onSubmitAnswer?: (formattedAnswer: string) => void;
+  onSubmitAnswer?: (formattedAnswer: string) => Promise<void>;
   isNewest?: boolean;
 };

   const [submitted, setSubmitted] = useState(false);
-  const disabled = alreadyAnswered || submitted || !isNewest;
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const disabled = alreadyAnswered || submitted || isSubmitting || !isNewest;

-  const handleSubmit = () => {
+  const handleSubmit = async () => {
     if (!onSubmitAnswer || !allQuestionsAnswered || disabled) return;

     const answers: Record<string, string> = {};
     for (const q of questions) {
       const sel = selections[q.question];
       answers[q.question] = Array.isArray(sel) ? sel.join(", ") : (sel as string);
     }

     const response = JSON.stringify({ questions, answers });
-    onSubmitAnswer(response);
-    setSubmitted(true);
+    try {
+      setIsSubmitting(true);
+      await onSubmitAnswer(response);
+      setSubmitted(true);
+    } finally {
+      setIsSubmitting(false);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/ask-user-question.tsx` around
lines 111 - 125, The handleSubmit function currently calls onSubmitAnswer and
immediately calls setSubmitted(true); instead change it to await the async
submission and only mark submitted on success: update the onSubmitAnswer
signature to return Promise<void>, add a local loading state (e.g.,
isSubmitting) toggled true before calling onSubmitAnswer and false in finally,
await onSubmitAnswer(response) inside handleSubmit, and only call
setSubmitted(true) after the awaited call resolves; also handle errors by not
setting submitted and optionally surfacing the error so the UI can allow
retries; key symbols: handleSubmit, onSubmitAnswer, setSubmitted.
components/backend/handlers/sessions.go (3)

673-676: ⚠️ Potential issue | 🟠 Major

Millisecond timestamps still allow duplicate session names.

Two create requests in the same millisecond still produce the same CR name here, and the Create() failure below is surfaced as a 500. Use Kubernetes generateName or retry on AlreadyExists so burst session creation does not fail spuriously.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 673 - 676, The current
session name generation (timestamp := time.Now().UnixMilli(); name :=
fmt.Sprintf("session-%d", timestamp)) can collide on burst creates; update the
session creation to use Kubernetes metadata GenerateName (e.g. set
ObjectMeta.GenerateName = "session-" so the API will append a unique suffix) or
implement a retry around the Create() call that specifically catches
IsAlreadyExists errors and retries with a new name (or backoff) until success;
modify the code paths that use the variables name/timestamp and the Create()
invocation to adopt GenerateName or the retry-on-AlreadyExists strategy.

439-440: ⚠️ Potential issue | 🟠 Major

Derive agent status after pagination, not before.

enrichAgentStatus() now runs while iterating the full namespace result set, even though this handler only returns paginatedSessions. Because derivation hits the event-log path, list latency scales with all running sessions instead of the page size. Enrich only the paginated slice after filtering/sorting/pagination.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 439 - 440,
enrichAgentStatus is being called for every session in the full namespace result
set, causing event-log lookups for all running sessions; move the call so it
only runs on the final paginated slice. After you perform filtering, sorting,
and pagination to produce paginatedSessions, iterate over that slice and call
enrichAgentStatus(&session) (or the equivalent loop that currently calls
enrichAgentStatus) only for those sessions to avoid scaling list latency with
the total namespace size.

44-46: ⚠️ Potential issue | 🔴 Critical

Namespace is still missing from the event-store lookup key.

DeriveAgentStatusFromEvents only accepts a bare session identifier, and the current callers pass the session name. Since the AG-UI store is also keyed by that value, two projects that create the same session name can read/write the same event log and derive the wrong status. Make the store key namespace-qualified end to end, or switch the store to use the CR UID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 44 - 46,
DeriveAgentStatusFromEvents currently accepts only a bare session identifier
(sessionID) which allows collisions across projects because callers pass session
name; update the function signature and all callers to include a
namespace-qualified key (e.g., namespace + "/" + sessionName) or switch to using
the unique CR UID instead of the name so the event-store lookup key is globally
unique; locate and update the function DeriveAgentStatusFromEvents and any
callers in the websocket/AG-UI integration to pass the new namespaced key (or CR
UID), and ensure the event-store read/write logic uses that same namespaced/UID
key end-to-end to prevent cross-project event mixing.
components/backend/websocket/agui_store.go (1)

202-248: ⚠️ Potential issue | 🟠 Major

Route status derivation through the canonical event loader.

This reads agui-events.jsonl directly and bypasses loadEvents() at Lines 164-195, which is the only path here that performs the legacy messages.jsonlagui-events.jsonl migration. Older sessions will therefore keep returning "" for status until some other endpoint happens to trigger migration. Reuse the same loader/migration path as replay.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_store.go` around lines 202 - 248,
DeriveAgentStatus currently reads agui-events.jsonl directly and bypasses the
migration done by loadEvents (the loader that handles messages.jsonl →
agui-events.jsonl); change DeriveAgentStatus to call loadEvents(sessionID) to
obtain the canonical event list (and let loadEvents perform migration if
needed), then iterate the returned events to compute and return the same status
string instead of manually opening agui-events.jsonl; ensure you handle
nil/empty/err from loadEvents the same way the existing callers expect
(returning "" on failure).
components/frontend/src/services/queries/use-sessions.ts (1)

56-60: ⚠️ Potential issue | 🟠 Major

Treat missing agentStatus as active for the 5s polling tier.

Line 58 only matches agentStatus === 'working'. agentStatus is optional, so newly started Running sessions that haven't derived a status yet still drop into the 15s bucket and the list can stay stale for an extra cycle. Keep missing/empty agentStatus in the 5s path as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/services/queries/use-sessions.ts` around lines 56 -
60, The current 5s polling tier predicate (the hasWorking = items.some(...)
check) only treats sessions with s.status?.agentStatus === 'working' as active;
change it to also treat a missing/empty agentStatus as active so newly started
Running sessions are included. Update the items.some predicate used to compute
hasWorking to return true when s.status?.phase === 'Running' and
(s.status?.agentStatus === 'working' OR s.status?.agentStatus is
null/undefined/empty). Keep the variable name hasWorking and the early return
(if (hasWorking) return 5000) unchanged.
components/frontend/src/components/ui/stream-message.tsx (1)

13-20: ⚠️ Potential issue | 🟠 Major

Type mismatch: onSubmitAnswer should return Promise<void>.

StreamMessageProps.onSubmitAnswer is typed as (formattedAnswer: string) => void, but the actual handler (sendToolAnswer in page.tsx) returns Promise<void>. This type inconsistency silently discards the Promise, preventing proper error handling in the component.

🐛 Proposed fix
 export type StreamMessageProps = {
   message: (MessageObject | ToolUseMessages | HierarchicalToolMessage) & { streaming?: boolean };
   onGoToResults?: () => void;
-  onSubmitAnswer?: (formattedAnswer: string) => void;
+  onSubmitAnswer?: (formattedAnswer: string) => Promise<void>;
   plainCard?: boolean;
   isNewest?: boolean;
   agentName?: string;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/stream-message.tsx` around lines 13 -
20, StreamMessageProps has onSubmitAnswer typed as (formattedAnswer: string) =>
void but the actual handler sendToolAnswer returns Promise<void>; update the
prop signature in StreamMessageProps to onSubmitAnswer?: (formattedAnswer:
string) => Promise<void> so callers can await it and surface errors; update any
usages (e.g., the StreamMessage component prop and where sendToolAnswer is
passed) to treat it as an async function and await or handle its Promise
accordingly.
components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx (1)

1401-1410: ⚠️ Potential issue | 🟡 Minor

Consider re-throwing errors from sendToolAnswer to allow UI components to handle failures.

The sendToolAnswer function catches errors and shows a toast but doesn't re-throw. This prevents the AskUserQuestionMessage component from knowing if the send failed, potentially leaving the UI in a submitted state when the answer wasn't actually sent.

🛡️ Proposed fix
   const sendToolAnswer = async (formattedAnswer: string) => {
     try {
       await aguiSendMessage(formattedAnswer, {
         type: "ask_user_question_response",
       });
     } catch (err) {
       toast.error(err instanceof Error ? err.message : "Failed to send answer");
+      throw err;
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/page.tsx
around lines 1401 - 1410, The sendToolAnswer function currently swallows errors
(catching them and showing a toast) so callers like AskUserQuestionMessage can't
detect failures; update sendToolAnswer (the function that calls aguiSendMessage)
to re-throw the caught error after showing the toast (or alternatively return a
rejected Promise/explicit failure value) so the AskUserQuestionMessage component
can handle the failure state; ensure you reference sendToolAnswer and
aguiSendMessage in the change and propagate the error back to the caller instead
of silently absorbing it.
components/frontend/src/components/agent-status-indicator.tsx (1)

25-105: 🧹 Nitpick | 🔵 Trivial

Consider adding exhaustive switch handling.

The switch statement covers all current AgentStatus values but lacks a default case. If new status values are added to AgentStatus in the future, this component will silently return undefined.

♻️ Suggested enhancement for compile-time safety
     case "idle":
       return (
         <div className={cn("flex items-center gap-1.5", className)}>
           <Circle
             className={cn(
               "text-gray-400",
               compact ? "w-3.5 h-3.5" : "w-4 h-4"
             )}
           />
           {!compact && (
             <span className="text-sm text-muted-foreground">Idle</span>
           )}
         </div>
       );
+
+    default: {
+      const _exhaustiveCheck: never = status;
+      return _exhaustiveCheck;
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/agent-status-indicator.tsx` around lines
25 - 105, The switch in the AgentStatusIndicator component (the switch(status)
handling "working", "waiting_input", "completed", "failed", "idle") is not
exhaustive and can return undefined if AgentStatus gains new values; add an
explicit default branch to enforce exhaustiveness — either call an
assertUnreachable(value) helper (throwing an error) to surface new statuses at
runtime/compile-time or return a safe fallback UI (e.g., null or a generic
Badge) so the component never returns undefined; implement the chosen approach
in the switch's default case and/or add an assertUnreachable function to the
module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/components/ui/stream-message.tsx`:
- Around line 22-25: The normalization logic in isAskUserQuestionTool is
duplicated; extract it into a shared utility function (e.g., export function
isAskUserQuestionTool(normalizedName: string | string): boolean from a
new/shared utils module) that encapsulates name.toLowerCase().replace(/[^a-z]/g,
"") === "askuserquestion", then replace the local implementations in
stream-message.tsx, use-agent-status.ts, and tool-message.tsx with imports of
that shared isAskUserQuestionTool to centralize behavior and avoid duplication
while preserving current semantics and signature.

In `@components/frontend/src/components/workspace-sections/sessions-section.tsx`:
- Around line 336-340: The current fallback passes
deriveAgentStatusFromPhase(phase) when session.status?.agentStatus is missing,
which maps transitional phases (Pending/Creating) to "idle"; change the ternary
so that when phase is a transitional phase (e.g., 'Pending' or 'Creating') and
session.status?.agentStatus is undefined, you do not call
deriveAgentStatusFromPhase but instead pass a neutral/explicit transitional
value (or undefined) into AgentStatusIndicator; update the conditional around
AgentStatusIndicator to first prefer session.status?.agentStatus, then handle
transitional phases explicitly, and only call deriveAgentStatusFromPhase(phase)
for non-transitional terminal phases.

In `@components/frontend/src/hooks/use-agent-status.ts`:
- Around line 35-53: The loop reads lastTc.function.name without guarding
against missing function data; update the check around PlatformToolCall lastTc
to ensure lastTc and lastTc.function and its name exist before calling
isAskUserQuestionTool. In practice, add a defensive guard (e.g., skip/continue
if lastTc is falsy or lastTc.function or lastTc.function.name is missing) and
only call isAskUserQuestionTool when the name is present; reference symbols:
messages, lastTc, PlatformToolCall/ToolCall, and isAskUserQuestionTool.

In `@components/runners/ambient-runner/ambient_runner/platform/prompts.py`:
- Around line 88-95: The module currently claims to be framework-agnostic but
embeds Claude-specific guidance (HUMAN_INPUT_INSTRUCTIONS) and unconditionally
appends it in build_workspace_context_prompt; update the module by either (A)
changing the top-level docstring to state this prompt is Claude-only (used by
resolve_workspace_prompt/Claude bridge) or (B) make
build_workspace_context_prompt accept a boolean flag (e.g.,
include_ask_user_instructions) or a capabilities set and only append
HUMAN_INPUT_INSTRUCTIONS when that flag/capability is true; update
resolve_workspace_prompt (the Claude importer) to pass true so behavior remains
unchanged for Claude.

---

Duplicate comments:
In `@components/backend/handlers/sessions.go`:
- Around line 673-676: The current session name generation (timestamp :=
time.Now().UnixMilli(); name := fmt.Sprintf("session-%d", timestamp)) can
collide on burst creates; update the session creation to use Kubernetes metadata
GenerateName (e.g. set ObjectMeta.GenerateName = "session-" so the API will
append a unique suffix) or implement a retry around the Create() call that
specifically catches IsAlreadyExists errors and retries with a new name (or
backoff) until success; modify the code paths that use the variables
name/timestamp and the Create() invocation to adopt GenerateName or the
retry-on-AlreadyExists strategy.
- Around line 439-440: enrichAgentStatus is being called for every session in
the full namespace result set, causing event-log lookups for all running
sessions; move the call so it only runs on the final paginated slice. After you
perform filtering, sorting, and pagination to produce paginatedSessions, iterate
over that slice and call enrichAgentStatus(&session) (or the equivalent loop
that currently calls enrichAgentStatus) only for those sessions to avoid scaling
list latency with the total namespace size.
- Around line 44-46: DeriveAgentStatusFromEvents currently accepts only a bare
session identifier (sessionID) which allows collisions across projects because
callers pass session name; update the function signature and all callers to
include a namespace-qualified key (e.g., namespace + "/" + sessionName) or
switch to using the unique CR UID instead of the name so the event-store lookup
key is globally unique; locate and update the function
DeriveAgentStatusFromEvents and any callers in the websocket/AG-UI integration
to pass the new namespaced key (or CR UID), and ensure the event-store
read/write logic uses that same namespaced/UID key end-to-end to prevent
cross-project event mixing.

In `@components/backend/websocket/agui_store.go`:
- Around line 202-248: DeriveAgentStatus currently reads agui-events.jsonl
directly and bypasses the migration done by loadEvents (the loader that handles
messages.jsonl → agui-events.jsonl); change DeriveAgentStatus to call
loadEvents(sessionID) to obtain the canonical event list (and let loadEvents
perform migration if needed), then iterate the returned events to compute and
return the same status string instead of manually opening agui-events.jsonl;
ensure you handle nil/empty/err from loadEvents the same way the existing
callers expect (returning "" on failure).

In `@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/page.tsx:
- Around line 1401-1410: The sendToolAnswer function currently swallows errors
(catching them and showing a toast) so callers like AskUserQuestionMessage can't
detect failures; update sendToolAnswer (the function that calls aguiSendMessage)
to re-throw the caught error after showing the toast (or alternatively return a
rejected Promise/explicit failure value) so the AskUserQuestionMessage component
can handle the failure state; ensure you reference sendToolAnswer and
aguiSendMessage in the change and propagate the error back to the caller instead
of silently absorbing it.

In `@components/frontend/src/components/agent-status-indicator.tsx`:
- Around line 25-105: The switch in the AgentStatusIndicator component (the
switch(status) handling "working", "waiting_input", "completed", "failed",
"idle") is not exhaustive and can return undefined if AgentStatus gains new
values; add an explicit default branch to enforce exhaustiveness — either call
an assertUnreachable(value) helper (throwing an error) to surface new statuses
at runtime/compile-time or return a safe fallback UI (e.g., null or a generic
Badge) so the component never returns undefined; implement the chosen approach
in the switch's default case and/or add an assertUnreachable function to the
module.

In `@components/frontend/src/components/session/ask-user-question.tsx`:
- Around line 278-292: The tab buttons in the render loop (the <button> that
calls setActiveTab(idx) and conditionally renders CheckCircle2 and
q.header/`Q${idx+1}`) are missing an explicit type and will default to submit
inside forms; update that JSX button element to include type="button" so clicks
do not trigger form submission, leaving the onClick, className logic, and
conditional children unchanged.
- Around line 111-125: The handleSubmit function currently calls onSubmitAnswer
and immediately calls setSubmitted(true); instead change it to await the async
submission and only mark submitted on success: update the onSubmitAnswer
signature to return Promise<void>, add a local loading state (e.g.,
isSubmitting) toggled true before calling onSubmitAnswer and false in finally,
await onSubmitAnswer(response) inside handleSubmit, and only call
setSubmitted(true) after the awaited call resolves; also handle errors by not
setting submitted and optionally surfacing the error so the UI can allow
retries; key symbols: handleSubmit, onSubmitAnswer, setSubmitted.

In `@components/frontend/src/components/ui/stream-message.tsx`:
- Around line 13-20: StreamMessageProps has onSubmitAnswer typed as
(formattedAnswer: string) => void but the actual handler sendToolAnswer returns
Promise<void>; update the prop signature in StreamMessageProps to
onSubmitAnswer?: (formattedAnswer: string) => Promise<void> so callers can await
it and surface errors; update any usages (e.g., the StreamMessage component prop
and where sendToolAnswer is passed) to treat it as an async function and await
or handle its Promise accordingly.

In `@components/frontend/src/services/queries/use-sessions.ts`:
- Around line 56-60: The current 5s polling tier predicate (the hasWorking =
items.some(...) check) only treats sessions with s.status?.agentStatus ===
'working' as active; change it to also treat a missing/empty agentStatus as
active so newly started Running sessions are included. Update the items.some
predicate used to compute hasWorking to return true when s.status?.phase ===
'Running' and (s.status?.agentStatus === 'working' OR s.status?.agentStatus is
null/undefined/empty). Keep the variable name hasWorking and the early return
(if (hasWorking) return 5000) unchanged.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 919-923: When you set self._halted = True and
self._halted_tool_call_id = current_tool_call_id inside the loop, also clear the
in-progress tool-call state (e.g., set current_tool_call_id = None or clear the
attribute that tracks the active tool call) before using continue so that the
finally block in the surrounding function does not emit a second
ToolCallEndEvent; update the same block where you set
self._halted/self._halted_tool_call_id and ensure the finally clause that emits
ToolCallEndEvent checks that current_tool_call_id (or the instance attribute
tracking the active tool call) is not None and matches expected state before
emitting.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 127-132: The interrupt check in ClaudeBridge currently reads the
bridge-wide flag self._adapter.halted which can be overwritten by concurrent
runs; instead capture the halt outcome for this specific run (e.g., return a
halted boolean from adapter.run(...) or have adapter.record_halt(thread_id) and
read that per-thread key) and use that value to decide whether to call
worker.interrupt(); update the ClaudeBridge logic (the branch checking
self._adapter.halted before awaiting worker.interrupt()) to use the run-specific
halted result or per-thread halted map keyed by thread_id so concurrent runs
cannot clobber each other.

In `@Makefile`:
- Around line 737-740: The trap currently uses `kill 0` and the port-forward
lines hard-code `ambient-code`; change the recipe to capture the two background
kubectl PIDs (e.g., start each `kubectl port-forward -n $(NAMESPACE) svc/... ...
&` and save their $$! values into variables like FRONTEND_PID and BACKEND_PID),
update both `-n ambient-code` occurrences to `-n $(NAMESPACE)`, and in the trap
kill those specific PIDs (kill -TERM $(FRONTEND_PID) $(BACKEND_PID) || true) and
wait for them instead of `kill 0`; keep the existing redirection and `wait`
behavior so the foreground Make job still blocks until both forwards exit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 771a53ed-d255-4167-ba2f-068bd6efe117

📥 Commits

Reviewing files that changed from the base of the PR and between 2075dcb and 957bc96.

⛔ Files ignored due to path filters (1)
  • components/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (26)
  • .gitignore
  • Makefile
  • components/backend/handlers/sessions.go
  • components/backend/main.go
  • components/backend/types/agui.go
  • components/backend/types/session.go
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/backend/websocket/agui_store_test.go
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
  • components/frontend/src/components/agent-status-indicator.tsx
  • components/frontend/src/components/session-status-dot.tsx
  • components/frontend/src/components/session/MessagesTab.tsx
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/components/ui/tool-message.tsx
  • components/frontend/src/components/workspace-sections/sessions-section.tsx
  • components/frontend/src/hooks/agui/types.ts
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/hooks/use-agui-stream.ts
  • components/frontend/src/services/queries/use-sessions.ts
  • components/frontend/src/types/agentic-session.ts
  • components/frontend/src/types/api/sessions.ts
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/ambient_runner/platform/prompts.py

Comment on lines +22 to +25
function isAskUserQuestionTool(name: string): boolean {
const normalized = name.toLowerCase().replace(/[^a-z]/g, "");
return normalized === "askuserquestion";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting isAskUserQuestionTool to a shared utility.

This exact normalization logic (toLowerCase().replace(/[^a-z]/g, "") === "askuserquestion") is duplicated in at least three files: use-agent-status.ts, stream-message.tsx, and tool-message.tsx. Extracting to a shared utility would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/ui/stream-message.tsx` around lines 22 -
25, The normalization logic in isAskUserQuestionTool is duplicated; extract it
into a shared utility function (e.g., export function
isAskUserQuestionTool(normalizedName: string | string): boolean from a
new/shared utils module) that encapsulates name.toLowerCase().replace(/[^a-z]/g,
"") === "askuserquestion", then replace the local implementations in
stream-message.tsx, use-agent-status.ts, and tool-message.tsx with imports of
that shared isAskUserQuestionTool to centralize behavior and avoid duplication
while preserving current semantics and signature.

Comment on lines +336 to +340
<AgentStatusIndicator status={
(phase === 'Completed' || phase === 'Failed' || phase === 'Stopped')
? deriveAgentStatusFromPhase(phase)
: (session.status?.agentStatus ?? deriveAgentStatusFromPhase(phase))
} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't fall back to idle for transitional phases.

When agentStatus is absent, Line 339 falls back to deriveAgentStatusFromPhase(phase). That helper returns idle for Pending and Creating, so newly starting sessions render as idle until the backend derives a status. Handle transitional phases explicitly instead of defaulting them to the idle label here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/workspace-sections/sessions-section.tsx`
around lines 336 - 340, The current fallback passes
deriveAgentStatusFromPhase(phase) when session.status?.agentStatus is missing,
which maps transitional phases (Pending/Creating) to "idle"; change the ternary
so that when phase is a transitional phase (e.g., 'Pending' or 'Creating') and
session.status?.agentStatus is undefined, you do not call
deriveAgentStatusFromPhase but instead pass a neutral/explicit transitional
value (or undefined) into AgentStatusIndicator; update the conditional around
AgentStatusIndicator to first prefer session.status?.agentStatus, then handle
transitional phases explicitly, and only call deriveAgentStatusFromPhase(phase)
for non-transitional terminal phases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 044420c — removed unused SessionPhaseBadge import and corrected AgentStatusIndicator usage with proper function signature.

Comment on lines +88 to +95
HUMAN_INPUT_INSTRUCTIONS = (
"## Human-in-the-Loop\n"
"When you need user input, a decision, or confirmation before proceeding, "
"you MUST use the AskUserQuestion tool. Do not ask questions in plain text "
"and wait for a response — the AskUserQuestion tool triggers platform "
"notifications and status indicators that help users know you need their "
"attention.\n\n"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Call sites for shared workspace prompt helpers ==="
rg -n -C2 '\b(resolve_workspace_prompt|build_workspace_context_prompt)\s*\('

echo
echo "=== AskUserQuestion tool registrations / handlers ==="
rg -n -C2 '\bAskUserQuestion\b|\bask_user_question\b|\bask-user-question\b'

echo
echo "=== Prompt builders / bridge files that may consume the shared prompt ==="
rg -n -C2 'Claude|ADK|prompt builder|resolve_workspace_prompt'

Repository: ambient-code/platform

Length of output: 50380


🏁 Script executed:

# Check the actual code at lines 218-219 and surrounding context
head -n 230 components/runners/ambient-runner/ambient_runner/platform/prompts.py | tail -n 30

# Search for ADK bridge
fd -t f "adk|ADK" --type file

# Look for any other bridges besides Claude
find components/runners/ambient-runner/ambient_runner/bridges -type d

Repository: ambient-code/platform

Length of output: 1554


🏁 Script executed:

# Search for whether AskUserQuestion is conditionally checked anywhere
rg -n "AskUserQuestion.*if|if.*AskUserQuestion|capability|BUILTIN_FRONTEND" \
  components/runners/ambient-runner/ambient_runner/platform/prompts.py

# Check what bridges exist
ls -la components/runners/ambient-runner/ambient_runner/bridges/

Repository: ambient-code/platform

Length of output: 405


🏁 Script executed:

# Look at the full build_workspace_context_prompt function to see the append
sed -n '124,230p' components/runners/ambient-runner/ambient_runner/platform/prompts.py

Repository: ambient-code/platform

Length of output: 3934


🏁 Script executed:

# Check if gemini_cli and langgraph bridges use resolve_workspace_prompt or call build_workspace_context_prompt
rg -n "resolve_workspace_prompt|build_workspace_context_prompt" \
  components/runners/ambient-runner/ambient_runner/bridges/gemini_cli/ \
  components/runners/ambient-runner/ambient_runner/bridges/langgraph/

# Check if these bridges have tool registration / AskUserQuestion capability
rg -n "AskUserQuestion|tools|BUILTIN_FRONTEND" \
  components/runners/ambient-runner/ambient_runner/bridges/gemini_cli/ \
  components/runners/ambient-runner/ambient_runner/bridges/langgraph/

Repository: ambient-code/platform

Length of output: 1579


🏁 Script executed:

# Check full structure of gemini_cli bridge
ls -la components/runners/ambient-runner/ambient_runner/bridges/gemini_cli/
cat components/runners/ambient-runner/ambient_runner/bridges/gemini_cli/__init__.py

# Check full structure of langgraph bridge
ls -la components/runners/ambient-runner/ambient_runner/bridges/langgraph/
cat components/runners/ambient-runner/ambient_runner/bridges/langgraph/__init__.py

Repository: ambient-code/platform

Length of output: 1358


🏁 Script executed:

# Check if there's any wrapper or entry point that selects which bridge to use
rg -n "gemini_cli|langgraph|resolve_workspace_prompt" \
  components/runners/ambient-runner/ambient_runner/runner.py \
  components/runners/ambient-runner/ambient_runner/main.py 2>/dev/null || true

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Check docstring of resolve_workspace_prompt to see what it actually says
sed -n '268,275p' components/runners/ambient-runner/ambient_runner/platform/prompts.py

# Check if there are any other imports or usages of build_workspace_context_prompt from non-Claude contexts
rg -n "from.*prompts.*import.*build_workspace_context_prompt|from.*prompts.*import.*resolve_workspace_prompt" \
  components/runners/ambient-runner/

# Check if any gemini_cli or langgraph code comments suggest future intent to use shared prompt
rg -n "workspace_prompt|shared.*prompt|workspace.*context" \
  components/runners/ambient-runner/ambient_runner/bridges/gemini_cli/ \
  components/runners/ambient-runner/ambient_runner/bridges/langgraph/

Repository: ambient-code/platform

Length of output: 1050


🏁 Script executed:

# List all Python files in the prompts module and their imports to understand intent
head -n 30 components/runners/ambient-runner/ambient_runner/platform/prompts.py

Repository: ambient-code/platform

Length of output: 1039


Fix misleading docstring and gate platform-specific guidance in shared prompt module.

The docstring falsely claims this prompt is "Shared helper used by both Claude and ADK bridge prompt builders," but no ADK bridge exists—only the Claude bridge imports resolve_workspace_prompt(). Additionally, build_workspace_context_prompt() unconditionally appends HUMAN_INPUT_INSTRUCTIONS, which hard-requires the AskUserQuestion tool (lines 88–95, 218). This creates a maintainability risk: the module presents itself as "framework-agnostic" (line 5) but embeds Claude-specific guidance. If future bridge implementations adopt this shared prompt, they will hit the same unavailable tool requirement. Either correct the docstring to document actual usage (Claude-only), or gate the AskUserQuestion guidance behind a capability flag before appending it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/platform/prompts.py` around
lines 88 - 95, The module currently claims to be framework-agnostic but embeds
Claude-specific guidance (HUMAN_INPUT_INSTRUCTIONS) and unconditionally appends
it in build_workspace_context_prompt; update the module by either (A) changing
the top-level docstring to state this prompt is Claude-only (used by
resolve_workspace_prompt/Claude bridge) or (B) make
build_workspace_context_prompt accept a boolean flag (e.g.,
include_ask_user_instructions) or a capabilities set and only append
HUMAN_INPUT_INSTRUCTIONS when that flag/capability is true; update
resolve_workspace_prompt (the Claude importer) to pass true so behavior remains
unchanged for Claude.

@Gkrumbach07
Copy link
Contributor Author

@ambient-fix

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/backend/websocket/agui_proxy.go (1)

94-104: ⚠️ Potential issue | 🟡 Minor

Cleanup logic may leave stale entries for namespaced keys.

The cleanupStaleSessions function deletes sessionProjectMap entries by sessionName, but the map is now keyed by namespacedSessionID (line 266). This mismatch means stale entries won't be cleaned up.

🐛 Proposed fix
 func cleanupStaleSessions() {
 	ticker := time.NewTicker(staleSessionCleanupInterval)
 	defer ticker.Stop()

 	for range ticker.C {
 		now := time.Now()
 		sessionLastSeen.Range(func(key, value interface{}) bool {
 			lastSeen := value.(time.Time)
 			if now.Sub(lastSeen) > staleSessionThreshold {
 				sessionName := key.(string)
 				sessionLastSeen.Delete(sessionName)
 				sessionPortMap.Delete(sessionName)
-				sessionProjectMap.Delete(sessionName)
+				// sessionProjectMap is keyed by namespacedSessionID;
+				// remove any entry whose suffix matches this session.
+				sessionProjectMap.Range(func(k, _ interface{}) bool {
+					if s, ok := k.(string); ok && strings.HasSuffix(s, "/"+sessionName) {
+						sessionProjectMap.Delete(k)
+					}
+					return true
+				})
 				// lastActivityUpdateTimes is keyed by "project/session";
 				// remove any entry whose suffix matches this session.
 				lastActivityUpdateTimes.Range(func(k, _ interface{}) bool {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_proxy.go` around lines 94 - 104,
cleanupStaleSessions is deleting sessionProjectMap by sessionName but that map
is keyed by namespacedSessionID; change the deletion to remove entries whose key
has the suffix "/"+sessionName (similar to the lastActivityUpdateTimes loop).
Specifically, in cleanupStaleSessions iterate sessionProjectMap.Range and for
any key k (string) where strings.HasSuffix(k, "/"+sessionName) call
sessionProjectMap.Delete(k) instead of sessionProjectMap.Delete(sessionName);
keep the existing deletes for sessionLastSeen and sessionPortMap as-is.
♻️ Duplicate comments (3)
components/backend/handlers/sessions.go (2)

677-680: ⚠️ Potential issue | 🟠 Major

UnixMilli() still leaves a collision path here.

Two creates in the same namespace within one millisecond will generate the same CR name, and Create() still turns the resulting AlreadyExists into a 500. Prefer Kubernetes generateName and return created.GetName() / ComputeAutoBranch(created.GetName()), or retry on errors.IsAlreadyExists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 677 - 680, The current
session name generation (timestamp := time.Now().UnixMilli(); name :=
fmt.Sprintf("session-%d", timestamp)) can collide if two creates occur in the
same millisecond; change the creation logic to use Kubernetes server-side name
generation by setting the object metadata GenerateName (e.g. "session-") instead
of precomputing name, call Create(), then return created.GetName() /
ComputeAutoBranch(created.GetName()); alternatively implement a small retry loop
around the client Create() that catches errors.IsAlreadyExists and retries a few
times before failing—update the code paths that reference name, Create(),
ComputeAutoBranch to use the created object's actual name.

443-445: ⚠️ Potential issue | 🟠 Major

Derive agentStatus only for the returned page.

This still hits the event log for every session before search/pagination, even though only paginatedSessions are returned. On larger namespaces, list latency now scales with the full result set instead of the page size.

♻️ Suggested change
-		// Derive agentStatus from event log (source of truth) for running sessions
-		enrichAgentStatus(&session)
-
 		session.AutoBranch = ComputeAutoBranch(item.GetName())
 
 		sessions = append(sessions, session)
 	}
@@
 	// Apply pagination
 	totalCount := len(sessions)
 	paginatedSessions, hasMore, nextOffset := paginateSessions(sessions, params.Offset, params.Limit)
+	for i := range paginatedSessions {
+		enrichAgentStatus(&paginatedSessions[i])
+	}

Also applies to: 459-461

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 443 - 445, The code
currently calls enrichAgentStatus(&session) for every session in the full result
set, causing event-log lookups to scale with the full dataset; change this so
agent status is derived only for the sessions being returned by moving the
enrichAgentStatus calls to after pagination and running it only over the
paginatedSessions slice (i.e., replace the existing per-session
enrichAgentStatus calls before search/pagination with a loop that calls
enrichAgentStatus(&s) for each s in paginatedSessions). Apply the same change
for the other occurrence referenced (the block around enrichAgentStatus at the
second location) so both places derive agentStatus only for the returned page.
components/backend/websocket/agui_store.go (1)

203-213: ⚠️ Potential issue | 🟠 Major

Don't bypass the legacy migration path here.

This opens agui-events.jsonl directly and returns "" when it is missing, but loadEvents() is the path that migrates legacy messages.jsonl sessions. Until another endpoint triggers that migration, older sessions lose derived agentStatus in list/detail views.

♻️ Suggested change
 	file, err := os.Open(path)
-	if err != nil {
+	if err != nil && os.IsNotExist(err) {
+		if mErr := MigrateLegacySessionToAGUI(sessionID); mErr == nil {
+			file, err = os.Open(path)
+		}
+	}
+	if err != nil {
 		return ""
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/websocket/agui_store.go` around lines 203 - 213,
DeriveAgentStatus currently opens agui-events.jsonl directly and returns empty
on missing files, skipping the legacy migration handled by loadEvents; change
DeriveAgentStatus (and its use of StateBaseDir/agui-events.jsonl) to call or
delegate to loadEvents for the session when agui-events.jsonl is not present so
the legacy messages.jsonl migration runs and agentStatus can be derived;
specifically, detect the missing-file case in DeriveAgentStatus, invoke
loadEvents(sessionID) to trigger migration/creation of agui-events.jsonl (or
reuse its returned event stream) and then proceed to compute the status instead
of returning "".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/components/session/ask-user-question.tsx`:
- Around line 211-239: The label for the "Other" option lacks keyboard
support—add an onKeyDown handler and keyboard semantics so keyboard-only users
can toggle it: on the label that currently uses onClick={() => !disabled &&
handleOtherToggle(q.question)}, add tabIndex={0}, role="button", and an
onKeyDown={(e) => { if (!disabled && (e.key === "Enter" || e.key === " ")) {
e.preventDefault(); handleOtherToggle(q.question); } }} and ensure the existing
onClick and disabled checks remain; keep stopping propagation on the Input
unchanged and continue using otherText, handleOtherTextChange, handleOtherToggle
and disabled as the referenced symbols.

---

Outside diff comments:
In `@components/backend/websocket/agui_proxy.go`:
- Around line 94-104: cleanupStaleSessions is deleting sessionProjectMap by
sessionName but that map is keyed by namespacedSessionID; change the deletion to
remove entries whose key has the suffix "/"+sessionName (similar to the
lastActivityUpdateTimes loop). Specifically, in cleanupStaleSessions iterate
sessionProjectMap.Range and for any key k (string) where strings.HasSuffix(k,
"/"+sessionName) call sessionProjectMap.Delete(k) instead of
sessionProjectMap.Delete(sessionName); keep the existing deletes for
sessionLastSeen and sessionPortMap as-is.

---

Duplicate comments:
In `@components/backend/handlers/sessions.go`:
- Around line 677-680: The current session name generation (timestamp :=
time.Now().UnixMilli(); name := fmt.Sprintf("session-%d", timestamp)) can
collide if two creates occur in the same millisecond; change the creation logic
to use Kubernetes server-side name generation by setting the object metadata
GenerateName (e.g. "session-") instead of precomputing name, call Create(), then
return created.GetName() / ComputeAutoBranch(created.GetName()); alternatively
implement a small retry loop around the client Create() that catches
errors.IsAlreadyExists and retries a few times before failing—update the code
paths that reference name, Create(), ComputeAutoBranch to use the created
object's actual name.
- Around line 443-445: The code currently calls enrichAgentStatus(&session) for
every session in the full result set, causing event-log lookups to scale with
the full dataset; change this so agent status is derived only for the sessions
being returned by moving the enrichAgentStatus calls to after pagination and
running it only over the paginatedSessions slice (i.e., replace the existing
per-session enrichAgentStatus calls before search/pagination with a loop that
calls enrichAgentStatus(&s) for each s in paginatedSessions). Apply the same
change for the other occurrence referenced (the block around enrichAgentStatus
at the second location) so both places derive agentStatus only for the returned
page.

In `@components/backend/websocket/agui_store.go`:
- Around line 203-213: DeriveAgentStatus currently opens agui-events.jsonl
directly and returns empty on missing files, skipping the legacy migration
handled by loadEvents; change DeriveAgentStatus (and its use of
StateBaseDir/agui-events.jsonl) to call or delegate to loadEvents for the
session when agui-events.jsonl is not present so the legacy messages.jsonl
migration runs and agentStatus can be derived; specifically, detect the
missing-file case in DeriveAgentStatus, invoke loadEvents(sessionID) to trigger
migration/creation of agui-events.jsonl (or reuse its returned event stream) and
then proceed to compute the status instead of returning "".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4726b9d-14ac-4e6e-97c9-2f492030e3bc

📥 Commits

Reviewing files that changed from the base of the PR and between 957bc96 and c674b9d.

📒 Files selected for processing (8)
  • components/backend/handlers/sessions.go
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/services/queries/use-sessions.ts

ambient-code bot pushed a commit that referenced this pull request Mar 11, 2026
- **CRITICAL**: Fix race condition in bridge.py by scoping halt state per-thread
  - Added `_halted_by_thread` dict to track halt state separately for each thread_id
  - Prevents concurrent runs from interfering with each other's halt detection
  - Clears thread-specific halt flag after worker interrupt

- **Fix**: Clear in-flight tool-call state on halt in adapter.py
  - Prevents duplicate ToolCallEndEvent emission in finally block
  - Resets current_tool_call_id and related state when halting

- **Accessibility**: Add keyboard support to "Other" option in ask-user-question
  - Added onKeyDown handler for Enter/Space keys
  - Added tabIndex and role="button" for keyboard navigation
  - Ensures compliance with a11y requirements

Addresses CodeRabbit review feedback from PR #871.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@Gkrumbach07
Copy link
Contributor Author

E2E test failures

 Ambient Session Management Tests
    ✓ should create workspace and session successfully (4900ms)
    Session Page UI
      1) should display session phase badge and header
      ✓ should display all sidebar accordion sections (639ms)
      ✓ should display breadcrumbs and navigate (737ms)
      ✓ should display chat area with input (145ms)
    Workspace Page
      ✓ should display workspace page with sessions list (402ms)
      ✓ should visit each workspace tab (sessions, sharing, keys, settings) (2862ms)
      ✓ should open create session dialog and interact with form (530ms)
      ✓ should show session details when clicking a session row (315ms)
    Projects List
      ✓ should display projects page with workspace list (264ms)
      ✓ should display status badges on workspace cards (99ms)
    Agent Interaction (Running State)
      - should complete full lifecycle with agent response
    Session Header Actions
      2) should open session header menu and interact with items
    Workspace Admin Tabs
      ✓ should interact with settings tab forms (2319ms)
      ✓ should interact with sharing tab (1316ms)
      ✓ should interact with keys tab (252ms)
    Session Header Menu Deep Interactions
      3) should open View details modal from three-dot menu
      4) should open Edit name dialog from three-dot menu
      5) should open Clone dialog from three-dot menu
      6) should interact with Export chat submenu from three-dot menu
    Chat Input Features
      ✓ should interact with toolbar buttons on session page (173ms)
      ✓ should trigger autocomplete with slash command (174ms)
      ✓ should navigate input history with arrow keys (147ms)
    Feedback Buttons
      ✓ should look for feedback buttons on agent messages (2191ms)
    Theme and Navigation
      ✓ should toggle theme through all options (exercises theme-toggle.tsx) (2799ms)
      ✓ should render navigation component (130ms)
    Session Page Modals
      7) "before each" hook for "should open AddContextModal via Add Context button and close with Escape"
    Session Header Three-Dot Menu Deep
      8) should click Refresh from three-dot menu
      9) should navigate Export chat submenu and click As Markdown
    Workspace Admin Form Interactions
      ✓ should interact with settings tab Runner API Keys and env vars (4318ms)
      ✓ should interact with keys tab Create Key dialog (351ms)
      ✓ should interact with sharing tab Grant Permission dialog (1758ms)
      ✓ should interact with feature flags toggle buttons (850ms)
    Workspace Admin Form Submissions
      ✓ should submit Runner API Keys form and verify save response (3022ms)
      ✓ should add env variable and submit Integration Secrets form (1948ms)
      ✓ should expand S3 storage config and interact with radio options (1923ms)
      ✓ should create an access key, verify one-time display, and delete it (331ms)
      ✓ should click Refresh button on keys tab (351ms)
      ✓ should grant permission, verify in table, and revoke it (2542ms)
      ✓ should click Refresh button on sharing tab (1535ms)
    Chat Input Deep Interactions
      ✓ should type slash command then dismiss with Escape (225ms)
      ✓ should test Ctrl+Space to open autocomplete (453ms)
      ✓ should interact with Agents and Commands toolbar buttons (179ms)
    Welcome Experience Interactions
      ✓ should interact with workflow cards and Load workflow link (155ms)
      ✓ should click workflow cards in the welcome grid (2165ms)
      10) should open View all workflows dropdown and interact with search
    Sessions List Actions
      ✓ should use search input on workspace sessions page (2163ms)
      ✓ should open session row dropdown menu and see actions (1461ms)
    Session Page Context Accordion
      11) "before each" hook for "should expand Context accordion and interact with Add Context"
    Feedback Buttons on Messages
      ✓ should find and click thumbs up button to open FeedbackModal (2233ms)
      ✓ should find and click thumbs down button and submit feedback (2175ms)
    Send Message While Pending
      12) should queue a message when session is Pending
⚠️ file /home/runner/work/platform/platform/e2e/.nyc_output/out.json has no coverage information
Did you forget to instrument your web application? Read https://github.com/cypress-io/code-coverage#instrument-your-application


  39 passing (3m)
  1 pending
  12 failing

  1) Ambient Session Management Tests
       Session Page UI
         should display session phase badge and header:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:109:76)

  2) Ambient Session Management Tests
       Session Header Actions
         should open session header menu and interact with items:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:329:76)

  3) Ambient Session Management Tests
       Session Header Menu Deep Interactions
         should open View details modal from three-dot menu:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:483:76)

  4) Ambient Session Management Tests
       Session Header Menu Deep Interactions
         should open Edit name dialog from three-dot menu:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:513:76)

  5) Ambient Session Management Tests
       Session Header Menu Deep Interactions
         should open Clone dialog from three-dot menu:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:551:76)

  6) Ambient Session Management Tests
       Session Header Menu Deep Interactions
         should interact with Export chat submenu from three-dot menu:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:591:76)

  7) Ambient Session Management Tests
       Session Page Modals
         "before each" hook for "should open AddContextModal via Add Context button and close with Escape":
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.

Because this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `Session Page Modals`
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:833:76)

  8) Ambient Session Management Tests
       Session Header Three-Dot Menu Deep
         should click Refresh from three-dot menu:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:1038:76)

  9) Ambient Session Management Tests
       Session Header Three-Dot Menu Deep
         should navigate Export chat submenu and click As Markdown:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:1058:76)

  10) Ambient Session Management Tests
       Welcome Experience Interactions
         should open View all workflows dropdown and interact with search:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:1632:76)

  11) Ambient Session Management Tests
       Session Page Context Accordion
         "before each" hook for "should expand Context accordion and interact with Add Context":
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.

Because this error occurred during a `before each` hook we are skipping the remaining tests in the current suite: `Session Page Context Accordion`
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:1728:76)

  12) Ambient Session Management Tests
       Send Message While Pending
         should queue a message when session is Pending:
     AssertionError: Timed out retrying after 10000ms: Expected to find element: `[data-testid="session-phase-badge"]`, but never found it.
      at Context.eval (webpack://vteam-e2e/./cypress/e2e/sessions.cy.ts:1897:0)




  (Results)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Tests:        58                                                                               │
  │ Passing:      39                                                                               │
  │ Failing:      12                                                                               │
  │ Pending:      1                                                                                │
  │ Skipped:      6                                                                                │
  │ Screenshots:  12                                                                               │
  │ Video:        true                                                                             │
  │ Duration:     3 minutes, 2 seconds                                                             │
  │ Spec Ran:     sessions.cy.ts                                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


  (Screenshots)

  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Page UI -- should display session phase b               
     adge and header (failed).png                                                                   
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Actions -- should open session hea               
     der menu and interact with items (failed).png                                                  
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Menu Deep Interactions -- should o               
     pen View details modal from three-dot menu (failed).png                                        
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Menu Deep Interactions -- should o               
     pen Edit name dialog from three-dot menu (failed).png                                          
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Menu Deep Interactions -- should o               
     pen Clone dialog from three-dot menu (failed).png                                              
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Menu Deep Interactions -- should i               
     nteract with Export chat submenu from three-dot menu (failed).png                              
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Page Modals -- should open AddContextModa               
     l via Add Context button and close with Escape -- before each hook (failed).png                
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Three-Dot Menu Deep -- should clic               
     k Refresh from three-dot menu (failed).png                                                     
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Header Three-Dot Menu Deep -- should navi               
     gate Export chat submenu and click As Markdown (failed).png                                    
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Welcome Experience Interactions -- should open Vi               
     ew all workflows dropdown and interact with search (failed).png                                
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Session Page Context Accordion -- should expand C               
     ontext accordion and interact with Add Context -- before each hook (failed).png                
  -  /home/runner/work/platform/platform/e2e/cypress/screenshots/sessions.cy.ts/Ambie     (1280x633)
     nt Session Management Tests -- Send Message While Pending -- should queue a mess               
     age when session is Pending (failed).png                                                       


  (Video)

  -  Video output: /home/runner/work/platform/platform/e2e/cypress/videos/sessions.cy.ts.mp4


tput: No value for $TERM and no -T specified
====================================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped  
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✖  sessions.cy.ts                           03:02       58       39       12        1        6 │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✖  1 of 1 failed (100%)                     03:02       58       39       12        1        6  

Gkrumbach07 and others added 11 commits March 11, 2026 20:09
- Introduced `SessionStatusDot` and `AgentStatusIndicator` components for improved visual representation of session and agent statuses.
- Updated `ProjectSessionDetailPage` to utilize new components, replacing the previous badge implementation.
- Enhanced `MessagesTab` to handle pending answers for `AskUserQuestion` tool responses, ensuring user input is sent correctly.
- Modified `StreamMessage` to render `AskUserQuestionMessage` for interactive question handling.
- Updated `ToolMessage` to generate summaries for `AskUserQuestion` tool calls, improving user feedback.

This update improves the user experience by providing clearer status indicators and better handling of interactive questions in the chat interface.
- Removed `package-lock.json` to streamline dependency management.
- Enhanced `AgenticSessionStatus` to include `agentStatus`, reflecting the current state of the agent.
- Updated `sessions.go` to parse and set `agentStatus` based on incoming status updates.
- Improved `agui_proxy.go` to manage `agentStatus` updates for various session events, ensuring accurate real-time status representation.
- Modified frontend components to handle `agentStatus` and integrate new functionalities for `AskUserQuestion` responses, improving user interaction and feedback.

This update enhances the overall session management and user experience by providing clearer status indicators and better handling of interactive questions.
- Changed session name generation from Unix timestamp to millisecond timestamp for improved safety during burst creation.
- Updated comments to clarify the purpose of the timestamp in the session naming process.

This change enhances the uniqueness of session names, reducing potential conflicts during rapid session creation.
- Remove accidentally tracked .claude/worktrees/ files and add to .gitignore
- Remove unused state variables in MessagesTab.tsx (interrupting, autocomplete*)
- Remove unused qIdx parameter from renderQuestionOptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused getPhaseColor import in session detail page (eslint error)
- Add retry.RetryOnConflict to updateAgentStatus to prevent concurrent
  goroutines from silently overwriting each other's CR status updates
- Handle unstructured.NestedMap errors explicitly
- Clear agentStatus to "idle" on all terminal phase transitions
  (Stopped, Completed, Failed) in both operator reconciler and pod monitor
  to prevent stale waiting_input status on abnormally terminated sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Derive agentStatus at query time from persisted event log instead of
  caching on the CR (eliminates goroutine race condition)
- Fix useAgentStatus hook to read raw AG-UI PlatformMessage format
  instead of processed ToolUseMessages (fixes detail page status)
- Emit TOOL_CALL_RESULT on next run for halted AskUserQuestion tool
  calls so frontend marks questions as answered
- Remove agentStatus field from CRD and CR update logic
- Remove animate-pulse from Running dot and waiting_input badge
- Add AgentStatus constants to types/agui.go

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline 3-value literals ("working" | "idle" | "waiting_input")
with exported StoredAgentStatus type alias in both frontend type files.
This resolves the type mismatch where AgentStatus has 5 values but the
CR status field only stores 3 (completed/failed are derived at query
time from phase, not stored).

Addresses reviewer feedback about inconsistent agentStatus typing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Limit event log read to last 64KB instead of loading entire file into
memory. Large event logs from long-running sessions could cause latency
on every ListSessions/GetSession call. Backward scan only needs recent
lifecycle events, so tail window is sufficient.

- Seek to file tail for files >64KB
- Skip partial first line after seek
- Read entire file if <=64KB

Addresses reviewer concern about unbounded file reads on every API call.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add test coverage for DeriveAgentStatus backward-scan logic including:
- Empty file returns empty status
- RUN_STARTED → working
- RUN_FINISHED → idle
- RUN_FINISHED with same-run AskUserQuestion → waiting_input
- RUN_FINISHED with different-run AskUserQuestion → idle
- RUN_ERROR → idle
- Case-insensitive AskUserQuestion detection
- Non-existent session returns empty status

All 8 test cases pass. Addresses reviewer feedback about missing test
coverage for complex stateful parsing logic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove extra blank lines in MessagesTab.tsx (lines 87-88)
- Replace unsafe double type cast with type guard in ask-user-question
- Move AskUserQuestionMessage from components/ui/ to components/session/
- Format agui_proxy.go with gofmt

The AskUserQuestionMessage component is business logic, not a UI
primitive, so it belongs in components/session/ rather than
components/ui/ (which is reserved for Shadcn primitives).

Type guard function isAskUserQuestionInput() is safer and clearer than
"as unknown as" double cast.

Addresses reviewer feedback on code organization and style.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Ambient Code Bot and others added 7 commits March 11, 2026 20:09
Remove old location after moving AskUserQuestionMessage to
components/session/. The import in stream-message.tsx has been updated
to the new path.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… fix tooltip accessibility

- Change SessionStatusDot tooltip trigger from span to button for proper accessibility (fixes frontend lint error)
- Remove all agentStatus writes from operator (reconciler.go and sessions.go) — agentStatus is now derived at query time from event logs, not stored in CR

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix onSubmitAnswer type signature to return Promise<void> for proper error handling
- Add null safety guard for tool call function name access
- Add type="button" to tab navigation to prevent form submission
- Make AskUserQuestion handleSubmit async with loading state
- Include missing/empty agentStatus in 5s polling tier
- Re-throw errors in sendToolAnswer for component error handling
- Fix namespace collision in event store by using namespace-qualified session IDs

Co-Authored-By: Claude <noreply@anthropic.com>
- **CRITICAL**: Fix race condition in bridge.py by scoping halt state per-thread
  - Added `_halted_by_thread` dict to track halt state separately for each thread_id
  - Prevents concurrent runs from interfering with each other's halt detection
  - Clears thread-specific halt flag after worker interrupt

- **Fix**: Clear in-flight tool-call state on halt in adapter.py
  - Prevents duplicate ToolCallEndEvent emission in finally block
  - Resets current_tool_call_id and related state when halting

- **Accessibility**: Add keyboard support to "Other" option in ask-user-question
  - Added onKeyDown handler for Enter/Space keys
  - Added tabIndex and role="button" for keyboard navigation
  - Ensures compliance with a11y requirements

Addresses CodeRabbit review feedback from PR #871.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tive check

Move enrichAgentStatus call after pagination to avoid processing all
sessions. Add exhaustive switch check in AgentStatusIndicator and
data-testid for session phase badge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused SessionPhaseBadge import from sessions-section.tsx
- Replace SessionPhaseBadge with AgentStatusIndicator using correct pattern
- Fix Makefile kind-port-forward to use $(NAMESPACE) variable instead of hardcoded "ambient-code"
- Simplify redundant conditional logic for agent status derivation

Co-Authored-By: Claude <noreply@anthropic.com>
@ambient-code ambient-code bot force-pushed the feat/human-in-the-loop branch from 536add3 to 044420c Compare March 11, 2026 20:19
@ambient-code
Copy link
Contributor

ambient-code bot commented Mar 11, 2026

PR Fix Report

Successfully addressed review feedback and fixed issues found during code review.

Changes Made

1. Removed unused import

  • Removed SessionPhaseBadge from sessions-section.tsx (was imported but replaced with AgentStatusIndicator)

2. Fixed Makefile namespace configuration

  • Changed hardcoded ambient-code namespace to use $(NAMESPACE) variable in kind-port-forward target
  • Now respects NAMESPACE=... environment variable like other Makefile targets

3. Fixed agent status display logic

  • Corrected function call signature: deriveAgentStatusFromPhase() only accepts 1 parameter (phase)
  • Simplified redundant conditional logic for terminal states
  • Used consistent pattern: session.status?.agentStatus ?? deriveAgentStatusFromPhase(phase)
  • Reduced redundant function calls from 2-3× per session to 1× per session in render loop

PR Status

  • Rebased onto latest main
  • CI passing (31/31 checks)
  • Lints passing (auto-fixed formatting)
  • Code review completed with /simplify
  • All issues addressed

Summary

This PR is ready for merge. All review feedback has been addressed, the code has been rebased, and CI is green.


🤖 Fixed by ambient-code bot

@ambient-code
Copy link
Contributor

ambient-code bot commented Mar 12, 2026

Review Queue — Not Ready to Merge

E2E tests are failing — 12 tests can't find the session-phase-badge element after your UI changes replaced the phase badge with AgentStatusIndicator. Bot reviews also identified:

  • Critical: Cross-namespace session collision in event store (needs namespace-qualified paths)
  • Major: Unbounded file reads in DeriveAgentStatus (performance issue)

To unblock: Fix the 12 failing E2E tests (update selectors for AgentStatusIndicator), address the session collision issue, and resolve the unbounded file read performance problem.

@Gkrumbach07 Gkrumbach07 merged commit 43a5d7d into main Mar 12, 2026
33 checks passed
@Gkrumbach07 Gkrumbach07 deleted the feat/human-in-the-loop branch March 12, 2026 13:03
Gkrumbach07 added a commit that referenced this pull request Mar 12, 2026
@Gkrumbach07 Gkrumbach07 restored the feat/human-in-the-loop branch March 12, 2026 16:56
Gkrumbach07 added a commit that referenced this pull request Mar 12, 2026
## Summary

- **AskUserQuestion UI**: Interactive question component with
single/multi-select options, freeform "Other" input, and multi-question
tabbed navigation
- **Agent status from event log**: Derive `agentStatus` at query time
from persisted AG-UI events instead of caching on the CR (eliminates
goroutine race conditions)
- **Frontend status fix**: `useAgentStatus` hook reads raw
`PlatformMessage.toolCalls[]` format, correctly detecting unanswered
AskUserQuestion tool calls
- **TOOL_CALL_RESULT emission**: Adapter emits `TOOL_CALL_RESULT` on
next run for halted tool calls so the frontend transitions questions to
answered state
- **Session status indicators**: `SessionStatusDot` and
`AgentStatusIndicator` components for detail and table views
- **CRD cleanup**: Removed `agentStatus` field from CRD schema — no
longer stored, only derived

Supersedes #725.

## Test plan

- [ ] Create session, send prompt that triggers AskUserQuestion
- [ ] Verify question UI appears with options
- [ ] Verify session table shows `waiting_input` status
- [ ] Verify detail page shows `waiting_input` status
- [ ] Submit answer, verify question transitions to answered (green)
state
- [ ] Verify agent resumes after answer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ambient Code Bot <bot@ambient-code.local>
Co-authored-by: ambient-code[bot] <ambient-code[bot]@users.noreply.github.com>
Gkrumbach07 added a commit that referenced this pull request Mar 13, 2026
- **AskUserQuestion UI**: Interactive question component with
single/multi-select options, freeform "Other" input, and multi-question
tabbed navigation
- **Agent status from event log**: Derive `agentStatus` at query time
from persisted AG-UI events instead of caching on the CR (eliminates
goroutine race conditions)
- **Frontend status fix**: `useAgentStatus` hook reads raw
`PlatformMessage.toolCalls[]` format, correctly detecting unanswered
AskUserQuestion tool calls
- **TOOL_CALL_RESULT emission**: Adapter emits `TOOL_CALL_RESULT` on
next run for halted tool calls so the frontend transitions questions to
answered state
- **Session status indicators**: `SessionStatusDot` and
`AgentStatusIndicator` components for detail and table views
- **CRD cleanup**: Removed `agentStatus` field from CRD schema — no
longer stored, only derived

Supersedes #725.

- [ ] Create session, send prompt that triggers AskUserQuestion
- [ ] Verify question UI appears with options
- [ ] Verify session table shows `waiting_input` status
- [ ] Verify detail page shows `waiting_input` status
- [ ] Submit answer, verify question transitions to answered (green)
state
- [ ] Verify agent resumes after answer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ambient Code Bot <bot@ambient-code.local>
Co-authored-by: ambient-code[bot] <ambient-code[bot]@users.noreply.github.com>
Gkrumbach07 added a commit that referenced this pull request Mar 13, 2026
## Summary

- **AskUserQuestion UI**: Interactive question component with
single/multi-select options, freeform "Other" input, and multi-question
tabbed navigation
- **Agent status from event log**: Derive `agentStatus` at query time
from persisted AG-UI events instead of caching on the CR (eliminates
goroutine race conditions)
- **Frontend status indicators**: `SessionStatusDot` and
`AgentStatusIndicator` components for detail and table views,
`useAgentStatus` hook reads raw `PlatformMessage.toolCalls[]` format
- **TOOL_CALL_RESULT emission**: Adapter emits `TOOL_CALL_RESULT` on
next run for halted tool calls so the frontend transitions questions to
answered state
- **Session list refresh**: `refetchOnMount: 'always'` so navigating
back to the sessions page fetches fresh data immediately

Supersedes #871 (which was reverted in #896 due to a
namespace-qualification bug in the event store). This version removes
the namespace-qualification refactor entirely, keeping only the HITL
feature changes.

## Changes from #871

Removed:
- Namespace-qualified event store paths (caused chat-lost-on-refresh
bug)
- `.gitignore` worktrees entry
- `Makefile` port-forward cleanup
- `components/package-lock.json` deletion
- `UnixMilli` session timestamp change

Added:
- `refetchOnMount: 'always'` on session list query for immediate refresh
on navigation

## Test plan

- [ ] Create session, send prompt that triggers AskUserQuestion
- [ ] Verify question UI appears with options
- [ ] Submit answer, verify question transitions to answered state
- [ ] Verify agent resumes after answer
- [ ] Session table shows correct agent status
(working/idle/waiting_input)
- [ ] Detail page shows correct agent status
- [ ] Refresh page — verify chat history is preserved (no namespace bug)
- [ ] Navigate away from session detail and back to sessions list —
verify list refreshes immediately

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ambient Code Bot <bot@ambient-code.local>
Co-authored-by: ambient-code[bot] <ambient-code[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant